How to display OptionSet values in human-readable form?
Asked Answered
P

5

20

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.

Peltier answered 3/3, 2017 at 21:2 Comment(6)
I regard this as a flaw in the console output. I would love for names to appear in the output. I don't agree that this is limited to option sets; for example, why can't the UIApplicationState log as .active? — However, I think you know as well as I do that this is not a genuine question; you know there's no answer and you're just moaning. You're right to moan, but that's not a Stack Overflow matter. :)Thermolysis
@matt, Actually I'm hoping there is some trick that I'm not aware of, and that rather than moaning I'll be able to do a happy-dance.Peltier
I'm thinking of making my class CustomStringConvertible and writing code to display these values, but it would be nasty and require manual maintenance.Peltier
Possibly related? Why does an @objc enum have a different description than a pure Swift enum?. I think the gist is that UIViewAnimationOptions is just an @objc OptionSet type which is just a wrapper around an NS_OPTIONS bitmask. I think that's just bridged to Swift as an underlying Integer without any underlying metadata.Buckskin
Isn't this possible to do in a generic way for all enums, using mirror?Bevins
@VaddadiKartick This article seems to hint that it is possible, but I'm having trouble applying it to a UIKit OptionSet type.Buckskin
P
4

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.

Peltier answered 30/6, 2019 at 20:40 Comment(1)
Is there a way we can get back the Toppings from the rawValue? One may store the rawValue in database & later to retrieve the exact Toppings.Antitype
I
5

This is how I did it.

public struct Toppings: OptionSet {
        public let rawValue: Int
        
        public static let cheese = Toppings(rawValue: 1 << 0)
        public static let onion = Toppings(rawValue: 1 << 1)
        public static let lettuce = Toppings(rawValue: 1 << 2)
        public static let pickles = Toppings(rawValue: 1 << 3)
        public static let tomatoes = Toppings(rawValue: 1 << 4)
        
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
    }
    
    extension Toppings: CustomStringConvertible {
        
        static public var debugDescriptions: [(Self, String)] = [
            (.cheese, "cheese"),
            (.onion, "onion"),
            (.lettuce, "lettuce"),
            (.pickles, "pickles"),
            (.tomatoes, "tomatoes")
        ]
        
        public var description: String {
            let result: [String] = Self.debugDescriptions.filter { contains($0.0) }.map { $0.1 }
            let printable = result.joined(separator: ", ")
            
            return "\(printable)"
        }
    }
Indehiscent answered 18/12, 2020 at 14:16 Comment(3)
Yeah, there's various ways to do this when you create a mapping between the values and the display names, but the crux of my question is avoiding having to do that. It adds a requirement that you maintain the OptionSet values and their display names as your optionSet grows over time.Peltier
@duncanC unfortunately, if you are using an OptionSet with an Int as a raw value you will need to maintain the string mapping somewhere. An OptionSet with rawValue of Int is a number, it has no understanding of what string it's supposed to be. The accepted answer is still doing a mapping but using an enum with a String type to store that mapping automatically.Earreach
Yup. So the short answer to my question: "Is there some clean way to map an OptionSet to the selected values... without adding messy code that would require me to maintain a table of display names for each flag" is no.Peltier
P
4

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.

Peltier answered 30/6, 2019 at 20:40 Comment(1)
Is there a way we can get back the Toppings from the rawValue? One may store the rawValue in database & later to retrieve the exact Toppings.Antitype
C
3

Here is one approach I've taken, using a dictionary and iterating over the keys. Not great, but it works.

struct MyOptionSet: OptionSet, Hashable, CustomStringConvertible {

    let rawValue: Int
    static let zero = MyOptionSet(rawValue: 1 << 0)
    static let one = MyOptionSet(rawValue: 1 << 1)
    static let two = MyOptionSet(rawValue: 1 << 2)
    static let three = MyOptionSet(rawValue: 1 << 3)

    var hashValue: Int {
        return self.rawValue
    }

    static var debugDescriptions: [MyOptionSet:String] = {
        var descriptions = [MyOptionSet:String]()
        descriptions[.zero] = "zero"
        descriptions[.one] = "one"
        descriptions[.two] = "two"
        descriptions[.three] = "three"
        return descriptions
    }()

    public var description: String {
        var result = [String]()
        for key in MyOptionSet.debugDescriptions.keys {
            guard self.contains(key),
                let description = MyOptionSet.debugDescriptions[key]
                else { continue }
            result.append(description)
        }
        return "MyOptionSet(rawValue: \(self.rawValue)) \(result)"
    }

}

let myOptionSet = MyOptionSet([.zero, .one, .two])

// prints MyOptionSet(rawValue: 7) ["two", "one", "zero"]
Carollcarolle answered 28/10, 2018 at 1:17 Comment(1)
Not sure I like the fact that "zero" is 0001, should be 0.Calcariferous
S
3

StrOptionSet Protocol:

  • Add a labels set property to test each label value on Self.

StrOptionSet Extension:

  • Filter out which is not intersected.
  • Return the label text as array.
  • Joined with "," as CustomStringConvertible::description

Here is the snippet:

protocol StrOptionSet : OptionSet, CustomStringConvertible {
    typealias Label = (Self, String)
    static var labels: [Label] { get }
}
extension StrOptionSet {
    var strs: [String] { return Self.labels
                                .filter{ (label: Label) in self.intersection(label.0).isEmpty == false }
                                .map{    (label: Label) in label.1 }
    }
    public var description: String { return strs.joined(separator: ",") }
}

Add the label set for target option set VTDecodeInfoFlags.

extension VTDecodeInfoFlags : StrOptionSet {
    static var labels: [Label] { return [
        (.asynchronous, "asynchronous"),
        (.frameDropped, "frameDropped"),
        (.imageBufferModifiable, "imageBufferModifiable")
    ]}
}

Use it

let flags: VTDecodeInfoFlags = [.asynchronous, .frameDropped]
print("flags:", flags) // output: flags: .asynchronous,frameDropped
Sandhog answered 21/10, 2019 at 7:42 Comment(1)
Nice implementation. It still bugs me that you have to maintain a structure that contains all the values and their names though. That's the part I'm hoping to avoid.Peltier
P
0
struct MyOptionSet: OptionSet {
    let rawValue: UInt
    static let healthcare   = MyOptionSet(rawValue: 1 << 0)
    static let worldPeace   = MyOptionSet(rawValue: 1 << 1)
    static let fixClimate   = MyOptionSet(rawValue: 1 << 2)
    static let exploreSpace = MyOptionSet(rawValue: 1 << 3)
}

extension MyOptionSet: CustomStringConvertible {
    static var debugDescriptions: [(Self, String)] = [
        (.healthcare, "healthcare"),
        (.worldPeace, "world peace"),
        (.fixClimate, "fix the climate"),
        (.exploreSpace, "explore space")
    ]

    var description: String {
        let result: [String] = Self.debugDescriptions.filter { contains($0.0) }.map { $0.1 }
        return "MyOptionSet(rawValue: \(self.rawValue)) \(result)"
    }
}

Usage

var myOptionSet: MyOptionSet = []
myOptionSet.insert(.healthcare)
print("here is my options: \(myOptionSet)")
Porfirioporgy answered 5/4, 2020 at 20:0 Comment(1)
Sure, you can manually create a description property that logs the values, but to paraphrase my question: "...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."Peltier

© 2022 - 2024 — McMap. All rights reserved.