OptionSet vs Enum


Greetings, traveler!

Have you ever used the OptionSet protocol? It doesn’t look like an everyday tool, but it can be helpful in specific cases.

What is an OptionSet?

This type presents a mathematical set interface to a bit set. This is a bitset type, where every bit represents an option. If you are familiar with Objective-C, you may know an OptionSet alternative called NS_OPTIONS.

UIKit and Foundation examples

In Swift standard libraries, you can find examples of the OptionSet like UIView.AutoresizingMask, UIView.AnimationOptions and JSONSerialization.ReadingOptions.

// UIView.AutoresizingMask

let view = UIView()
view.autoresizingMask = [
    .flexibleRightMargin,
    .flexibleLeftMargin
]

// UIView.AnimationOptions

UIView.animate(
    withDuration: 0.3,
    delay: 0.5,
    options: .autoreverse
) {}

// JSONSerialization.ReadingOptions

let jsonObject: [String: Any] = [
    "user": "Bob",
    "age": 30,
    "info": [
        "id": "11",
        "rights": "admin"
    ],
]
let jsonData = try JSONSerialization.data(
    withJSONObject: jsonObject,
    options: .prettyPrinted
)

Custom OptionSet

You can create your own OptionSet. The only thing that needs to be done is to create the associated type rawValue to exhibit the type used for comparison. The rawValue must be of a type that conforms to the FixedWidthInteger protocol. You can use UInt8 or Int. If you want to use UInt8, remember that you can face additional challenges after reaching a 255 value.

All possible combinations can be stored in 4 bits of memory, each of which can take the value 0 or 1 depending on whether the additional option is selected. Like this:

  • First option: 0001
  • Second option: 0010
  • Third option: 0100
  • Fourth option 1000

So, we can create something like this:

struct UserRight: OptionSet {
    
    let rawValue: Int
    
    static let create = UserRight(rawValue: 1)
    static let edit = UserRight(rawValue: 2)
    static let read = UserRight(rawValue: 4)
    static let delete = UserRight(rawValue: 8)
    
}

Bitwise shift operators

But this is not convenient. Here is where bitwise shift operators come to the rescue. The bitwise left shift operator << and bitwise right shift operator >> move all bits in a number to the left or the right. Using them will make it to multiply or divide an integer by a factor of two. Shifting an integer’s bits to the left by one position doubles its value, whereas shifting it to the right by one position halves its value. Shifting to the right will have no effect if you reach the 0000 value.

struct UserRight: OptionSet {

    let rawValue: Int

    static let create = UserRight(rawValue: 1 << 1)
    static let edit = UserRight(rawValue: 1 << 2)
    static let read = UserRight(rawValue: 1 << 3)
    static let delete = UserRight(rawValue: 1 << 4)

}

OptionSet vs Enum

While OptionSet can look similar to Enum, this entity is intended for a different purpose. We can use an OptionSet type option as a set of multiple parameters. Of course, you can do such things with Enum too, but it wasn’t designed to be used like this. Check out this example:

extension UserRight {
    
    static let admin: UserRight = [.create, .edit, .read, .delete]
    static let editor: UserRight = [.edit, .read]
    
}

Now, we can write such a code:

func canRead(_ userRights: UserRight) -> Bool {
    userRights.contains(.read)
}

func canDelete(_ userRights: UserRight) -> Bool {
    userRights.contains(.delete)
}

canRead(.editor) // true
canDelete(.admin) // true
canDelete(.editor) // false

Printable OptionSet with description

If you want to print out the option value to the console, you can add the
CustomStringConvertible protocol conformance to your OptionSet. Consider this example:

extension UserRight: CustomStringConvertible {
    
    var description: String {
        Self.descriptions
            .filter { contains($0.0) }
            .map { $0.1 }
            .joined(separator: ", ")
    }
    
    private static var descriptions: [(Self, String)] {
        [
            (.create, "create"),
            (.edit, "edit"),
            (.read, "read"),
            (.delete, "delete"),
        ]
    }

}

let admin = UserRight.admin
admin.description // create, edit, read, delete

Custom protocol with Enum benefits

You can create a custom protocol to replicate the OptionSet behavior but with some convenient additions.

Create the OptionProtocol, which conforms to some other protocols.

  • RawRepresentale — to gain the ability to be converted to and from an associated raw value.
  • Hashable — for use with Set.
  • CaseIterable — to get access to a static container with all cases.
protocol OptionProtocol: RawRepresentable, Hashable, CaseIterable {}

Now, we can use Enum to create something that conforms to our protocol.

enum UserRightOption: String, OptionProtocol {
    case create, edit, read, delete
}

For convenience, we can create a typealias.

typealias UserRights = Set<UserRightOption>

We can create an extension to build OptionSet-like static properties.

extension Set where Element == UserRightOption {

    static var editor: UserRights {
        [.read, .edit]
    }
    
}

And since our type conforms to the CaseIterable protocol, we can create a property to store all the cases.

extension Set where Element == UserRightOption {

    static var admin: UserRights {
        Set(Element.allCases)
    }
    
}

Using the CaseIterable protocol, we can also automatically generate the bitset values for all of these cases to create a similar value for any Set containing our type. To do this, we can enumerate these values and then, while using the loop, use the bitwise OR | operator. The bitwise OR operator compares the bits of two numbers. Since it returns a new number whose bits are set to 1 if the bits are equal to 1 in either input number, we can immediately assign this value to the rawValue property.

extension Set where Element: OptionProtocol {
    
    var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() {
            if self.contains(element) {
                rawValue |= (1 << index)
            }
        }

        return rawValue
    }
    
}

Conclusion

We rarely encounter the OptionSet protocol in projects. However, it is a really convenient tool when we may want to use operations that a Set can offer, like contains, insert, remove, symmetricDifference, unionintersection, etc.