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
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)"
}
}
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)"
}
}
var myOptionSet: MyOptionSet = []
myOptionSet.insert(.healthcare)
print("here is my options: \(myOptionSet)")
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"]
StrOptionSet Protocol:
StrOptionSet Extension:
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
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 OptionSet
s.