iosswiftoptionsettype

How to display OptionSet values in human-readable form?


Swift has the OptionSet type, which basically adds set operations to C-Style bit flags. Apple is using them pretty extensively in their frameworks. Examples include the options parameter in animate(withDuration:delay:options:animations:completion:).

On the plus side, it lets you use clean code like:

options: [.allowAnimatedContent, .curveEaseIn]

However, there is a downside as well.

If I want to display the specified values of an OptionSet, there doesn't seem to be a clean way to do it:

let options: UIViewAnimationOptions = [.allowAnimatedContent, .curveEaseIn]
print("options = " + String(describing: options))

Displays the very unhelpful message:

options = UIViewAnimationOptions(rawValue: 65664)

The docs for some of these bit fields expresses the constant as a power-of-two value:

flag0    = Flags(rawValue: 1 << 0)

But the docs for my example OptionSet, UIViewAnimationOptions, doesn't tell you anything about the numeric value of these flags and figuring out bits from decimal numbers is not straightforward.

Question:

Is there some clean way to map an OptionSet to the selected values?

My desired output would be something like:

options = UIViewAnimationOptions([.allowAnimatedContent, .curveEaseIn])

But I can't think of a way to do this without adding messy code that would require me to maintain a table of display names for each flag.

(I'm interested in doing this for both system frameworks and custom OptionSets I create in my own code.)

Enums let you have both a name and a raw value for the enum, but those don't support the set functions you get with OptionSets.


Solution

  • This article in NSHipster gives an alternative to OptionSet that offers all the features of an OptionSet, plus easy logging:

    https://nshipster.com/optionset/

    If you simply add a requirement that the Option type be CustomStringConvertible, you can log Sets of this type very cleanly. Below is the code from the NSHipster site - the only change being the addition of CustomStringConvertible conformance to the Option class

    protocol Option: RawRepresentable, Hashable, CaseIterable, CustomStringConvertible {}
    
    enum Topping: String, Option {
        case pepperoni, onions, bacon,
        extraCheese, greenPeppers, pineapple
    
        //I added this computed property to make the class conform to CustomStringConvertible
        var description: String {
            return ".\(self.rawValue)"
        }
    }
    
    extension Set where Element == Topping {
        static var meatLovers: Set<Topping> {
            return [.pepperoni, .bacon]
        }
    
        static var hawaiian: Set<Topping> {
            return [.pineapple, .bacon]
        }
    
        static var all: Set<Topping> {
            return Set(Element.allCases)
        }
    }
    
    typealias Toppings = Set<Topping>
    
    extension Set where Element: Option {
        var rawValue: Int {
            var rawValue = 0
            for (index, element) in Element.allCases.enumerated() {
                if self.contains(element) {
                    rawValue |= (1 << index)
                }
            }
            return rawValue
        }
    }
    

    Then using it:

    let toppings: Set<Topping> = [.onions, .bacon]
    
    print("toppings = \(toppings), rawValue = \(toppings.rawValue)")
    

    That outputs

    toppings = [.onions, .bacon], rawValue = 6

    Just like you want it to.

    That works because a Set displays its members as a comma-delimited list inside square brackets, and uses the description property of each set member to display that member. The description property simply displays each item (the enum's name as a String) with a . prefix

    And since the rawValue of a Set<Option> is the same as an OptionSet with the same list of values, you can convert between them readily.

    I wish Swift would just make this a native language feature for OptionSets.