Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
298 lines (242 sloc) 8.58 KB
title author category excerpt revisions status
OptionSet
Mattt
Swift
Objective-C uses the `NS_OPTIONS` macro to define set of values that may be combined together. Swift imports those types as structures conforming to the `OptionSet` protocol. But could new language features in Swift provide a better option?
2014-09-09 2018-11-07
First Publication
Updated for Swift 4.2
swift reviewed
4.2
November 7, 2018

Objective-C uses the NS_OPTIONS macro to define option types, or sets of values that may be combined together. For example, values in the UIViewAutoresizing type in UIKit can be combined with the bitwise OR operator (|) and passed to the autoresizingMask property of a UIView to specify which margins and dimensions should automatically resize:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

Swift imports this and other types defined using the NS_OPTIONS macro as a structure that conforms to the OptionSet protocol.

extension UIView {
    struct AutoresizingMask: OptionSet {
        init(rawValue: UInt)

        static var flexibleLeftMargin: UIView.AutoresizingMask
        static var flexibleWidth: UIView.AutoresizingMask
        static var flexibleRightMargin: UIView.AutoresizingMask
        static var flexibleTopMargin: UIView.AutoresizingMask
        static var flexibleHeight: UIView.AutoresizingMask
        static var flexibleBottomMargin: UIView.AutoresizingMask
    }
}

{% info %} The renaming and nesting of imported types are the result of a separate mechanism. {% endinfo %}

At the time OptionSet was introduced (and RawOptionSetType before it), this was the best encapsulation that the language could provide. Towards the end of this article, we'll demonstrate how to take advantage of language features added in Swift 4.2 to improve upon OptionSet.

...but that's getting ahead of ourselves.

This week on NSHipster, let's take a by-the-books look at using imported OptionSet types, and how you can create your own. After that, we'll offer a different option for setting options.

Working with Imported Option Set Types

According to the documentation, there are over 300 types in Apple SDKs that conform to OptionSet, from ARHitTestResult.ResultType to XMLNode.Options.

No matter which one you're working with, the way you use them is always the same:

To specify a single option, pass it directly (Swift can infer the type when setting a property so you can omit everything up to the leading dot):

view.autoresizingMask = .flexibleHeight

OptionSet conforms to the SetAlgebra protocol, so to you can specify multiple options with an array literal --- no bitwise operations required:

view.autoresizingMask = [.flexibleHeight, .flexibleWidth]

To specify no options, pass an empty array literal ([]):

view.autoresizingMask = [] // no options

Declaring Your Own Option Set Types

You might consider creating your own option set type if you have a property that stores combinations from a closed set of values and you want that combination to be stored efficiently using a bitset.

To do this, declare a new structure that adopts the OptionSet protocol with a required rawValue instance property and type properties for each of the values you wish to represent. The raw values of these are initialized with increasing powers of 2, which can be constructed using the left bitshift (<<) operation with incrementing right-hand side values. You can also specify named aliases for specific combinations of values.

For example, here's how you might represent topping options for a pizza:

struct Toppings: OptionSet {
    let rawValue: Int

    static let pepperoni    = Toppings(rawValue: 1 << 0)
    static let onions       = Toppings(rawValue: 1 << 1)
    static let bacon        = Toppings(rawValue: 1 << 2)
    static let extraCheese  = Toppings(rawValue: 1 << 3)
    static let greenPeppers = Toppings(rawValue: 1 << 4)
    static let pineapple    = Toppings(rawValue: 1 << 5)

    static let meatLovers: Toppings = [.pepperoni, .bacon]
    static let hawaiian: Toppings = [.pineapple, .bacon]
    static let all: Toppings = [
        .pepperoni, .onions, .bacon,
        .extraCheese, .greenPeppers, .pineapple
    ]
}

Taken into a larger example for context:

struct Pizza {
    enum Style {
        case neapolitan, sicilian, newHaven, deepDish
    }

    struct Toppings: OptionSet { ... }

    let diameter: Int
    let style: Style
    let toppings: Toppings

    init(inchesInDiameter diameter: Int,
         style: Style,
         toppings: Toppings = [])
    {
        self.diameter = diameter
        self.style = style
        self.toppings = toppings
    }
}

let dinner = Pizza(inchesInDiameter: 12,
                   style: .neapolitan,
                   toppings: [.greenPeppers, .pineapple])

Another advantage of OptionSet conforming to SetAlgebra is that you can perform set operations like determining membership, inserting and removing elements, and forming unions and intersections. This makes it easy to, for example, determine whether the pizza toppings are vegetarian-friendly:

extension Pizza {
    var isVegetarian: Bool {
        return toppings.isDisjoint(with: [.pepperoni, .bacon])
    }
}

dinner.isVegetarian // true

A Fresh Take on an Old Classic

Alright, now that you know how to use OptionSet, let's show you how not to use OptionSet.

As we mentioned before, new language features in Swift 4.2 make it possible to have our cake pizza pie and eat it too.

First, declare a new Option protocol that inherits RawRepresentable, Hashable, and CaseIterable.

protocol Option: RawRepresentable, Hashable, CaseIterable {}

Next, declare an enumeration with String raw values that adopts the Option protocol:

enum Topping: String, Option {
    case pepperoni, onions, bacon,
         extraCheese, greenPeppers, pineapple
}

Compare the structure declaration from before to the following enumeration. Much nicer, right? Just wait --- it gets even better.

Automatic synthesis of Hashable provides effortless usage with Set, which gets us halfway to the functionality of OptionSet. Using conditional conformance, we can create an extension for any Set whose element is a Topping and define our named topping combos. As a bonus, CaseIterable makes it easy to order a pizza with "the works":

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>

And that's not all CaseIterable has up its sleeves; by enumerating over the allCases type property, we can automatically generate the bitset values for each case, which we can combine to produce the equivalent rawValue for any Set containing Option types:

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
    }
}

Because OptionSet and Set both conform to SetAlgebra our new Topping implementation can be swapped in for the original one without needing to change anything about the Pizza itself.

{% warning %} This approach assumes that the order of cases provided by CaseIterable is stable across launches. If it isn't, the generated raw value for combinations of options may be inconsistent. {% endwarning %}


So, to summarize: you're likely to encounter OptionSet when working with Apple SDKs in Swift. And although you could create your own structure that conforms to OptionSet, you probably don't need to. You could use the fancy approach outlined at the end of this article, or do with something more straightforward.

Whichever option you choose, you should be all set.