Home Understanding Swift OptionSet, a Flexible Data Type for Managing Multiple Choices
Post
Cancel

Understanding Swift OptionSet, a Flexible Data Type for Managing Multiple Choices

TLDR

Option sets can be thought of as an extension of enums that allow multiple cases to be true simultaneously

As Swift developers we have various tools at our disposal to handle different choice modeling scenarios effectively. Two commonly used constructs for modeling options and choices are enums and option sets. While enums are widely popular and powerful, there are cases where option sets offer us more flexibility.

What is an Option Set?

An option set is a data type that represents a set of distinct options or flags using bit-wise operations. I know that’s a mouthful but let’s break down the technical jargon.

For simplicity, it wouldn’t be wrong if you thought of Option sets as essentially enums that allow multiple cases to be true at once. They provide us the ability to model scenarios where we need multiple options to be combinable or where multiple cases can be true simultaneously.

Working with Option Set

To define an option set in Swift, we create a new struct and adopt the OptionSet protocol. This protocol requires the conforming type to be a RawRepresentable enumeration, where the raw value should be an integer data type (Int, UInt, Int8, UInt8, etc.). Let’s see how it’s done:

1
2
3
4
5
6
7
8
9
10
11
// Define an Option Set for Pizza Toppings
struct PizzaToppings: OptionSet {
    let rawValue: Int

    static let cheese = PizzaToppings(rawValue: 1 << 0)
    static let pepperoni = PizzaToppings(rawValue: 1 << 1)
    static let mushrooms = PizzaToppings(rawValue: 1 << 2)
    static let onions = PizzaToppings(rawValue: 1 << 3)
    static let bellPeppers = PizzaToppings(rawValue: 1 << 4)
    // Add more toppings as needed
}

In this example, we’ve defined a simple option set called PizzaToppings with five options: cheese, pepperoni, mushrooms, onions, and bellPeppers. Each option is represented using a different bit position in the underlying Int raw value. But let’s go a step further and solidify this example even more with a real world scenario.

Pizza 🍕 Example

Let’s say we wanted to represent a pizza object in Swift. Our pizza must have a 'size' property, and there can only be one size, such as 'small', 'medium', or 'large.' However, we also want our pizza to have a 'toppings' property. In this case, our pizza can have multiple toppings, as who would settle for just one topping on their pizza? This example serves as an excellent opportunity to demonstrate the appropriate use of option sets versus enums and the inherent flexibility that option sets offers us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Define an Option Set for Pizza Toppings
struct PizzaToppings: OptionSet {
    let rawValue: Int

    static let cheese = PizzaToppings(rawValue: 1 << 0)
    static let pepperoni = PizzaToppings(rawValue: 1 << 1)
    static let mushrooms = PizzaToppings(rawValue: 1 << 2)
    static let onions = PizzaToppings(rawValue: 1 << 3)
    static let bellPeppers = PizzaToppings(rawValue: 1 << 4)
    // Add more toppings as needed
}

// Define an Enum for Pizza Size
enum PizzaSize: String {
    case small
    case medium
    case large
}

// Define a Pizza struct
struct Pizza {
    let size: PizzaSize
    var toppings: PizzaToppings
}

// Create a pizza
var myPizza = Pizza(size: .medium, toppings: [.cheese, .mushrooms])

// Add more toppings
myPizza.toppings.insert(.pepperoni)

// Check if the pizza has onions
if myPizza.toppings.contains(.onions) {
    print("This pizza has onions.")
} else {
    print("This pizza does not have onions.")
}

// Display the pizza details
print("Size: \(myPizza.size.rawValue.capitalized)")
print("Toppings: \(myPizza.toppings)")

In the above code We created a Pizza struct that contains the size and toppings properties just like we wanted. We then created a medium sized pizza with cheese and mushroom toppings. Later, we add pepperoni as an additional topping to the pizza.

Finally, we demonstrate how to check if the pizza has onions using the contains() method of the option set, and then display the pizza details, including the size and toppings.

Although we can initialize an Optionset with an array literal syntax like this pizzaToppings = []. The Optionset protocol is actually more like a Set than an array, therefore they can be manipulated using set operations, such as union, intersection, and subtraction, making it easy to combine, check, and modify your set of options. Let’s play with the various methods and operators we can apply on them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Define an Option Set for Pizza Toppings
struct PizzaToppings: OptionSet {
    let rawValue: Int

    static let cheese = PizzaToppings(rawValue: 1 << 0)
    static let pepperoni = PizzaToppings(rawValue: 1 << 1)
    static let mushrooms = PizzaToppings(rawValue: 1 << 2)
    static let onions = PizzaToppings(rawValue: 1 << 3)
    static let bellPeppers = PizzaToppings(rawValue: 1 << 4)
    // Add more toppings as needed
}

// Create a pizza with some toppings
var myPizza: PizzaToppings = [.cheese, .pepperoni]

// Add more toppings to the pizza
myPizza.insert(.mushrooms)

// Check if the pizza has onions
if myPizza.contains(.onions) {
    print("This pizza has onions.")
} else {
    print("This pizza does not have onions.")
}

// Define a set of all available toppings
let allToppings: PizzaToppings = [.cheese, .pepperoni, .mushrooms, .onions, .bellPeppers]

// Get the toppings common to the pizza and the available toppings
let commonToppings = myPizza.intersection(allToppings)

// Remove a topping from the pizza
myPizza.subtract(.pepperoni)

// Get the toppings that are not present in the pizza but available
let missingToppings = allToppings.subtracting(myPizza)

// Display the pizza details
print("Selected Toppings: \(myPizza)")
print("Common Toppings: \(commonToppings)")
print("Missing Toppings: \(missingToppings)")

Here’s the list of the various set methods we just applied on our pizzaToppings optionset

  1. We add mushrooms to the pizza using the insert() method.
  2. We check if the pizza has onions using the contains() method.
  3. We define a set allToppings representing all available toppings.
  4. We find the toppings common to the pizza and the available toppings using the intersection() method and store them in commonToppings.
  5. We remove the topping pepperoni from the pizza using the subtract() method.
  6. We find the toppings that are available but not present in the pizza using the subtracting() method and store them in missingToppings.

Comparing Option Sets with Enums

Enums and option sets share similarities, but they serve different purposes. Enums are best suited for when we are trying to represent mutually exclusive choices, where only one case can be true at a time. On the other hand, option sets excel in scenarios where we know multiple cases can be active or true simultaneously.

let’s say we have a UI element that allows users to customize their text appearance. We can use an enum for text style options:

1
2
3
4
5
enum TextStyle {
    case bold
    case italic
    case underline
}

In this case, users can only choose one text style at a time:

1
let selectedStyle: TextStyle = .bold

But what if we want to allow users to choose multiple text decoration options, option sets become the ideal choice in this scenario:

1
2
3
4
5
6
7
struct TextDecoration: OptionSet {
    let rawValue: Int

    static let bold = TextDecoration(rawValue: 1 << 0)
    static let italic = TextDecoration(rawValue: 1 << 1)
    static let underline = TextDecoration(rawValue: 1 << 2)
}

Now, our users can select and apply multiple text decorations simultaneously:

1
var selectedDecorations: TextDecoration = [.bold, .underline]

Closing Thought

OptionSet is an underrated swift data type that is used in a lot of swift’s standard libraries by apple and as we have explored together they provide us with a flexible and type safe way to work with a set of options where multiple cases can be active or true simultaneously. Understanding when to use enums and when to opt for option sets will help us write better type safe code. Thanks for reading ✌️

This post is licensed under CC BY 4.0 by the author.