OptionSet
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 UIView
type in UIKit
can be combined with the bitwise OR operator (|
)
and passed to the autoresizing
property of a UIView
to specify which margins and dimensions should automatically resize:
typedef NS_OPTIONS(NSUInteger, UIView Autoresizing) {
UIView Autoresizing None = 0,
UIView Autoresizing Flexible Left Margin = 1 << 0,
UIView Autoresizing Flexible Width = 1 << 1,
UIView Autoresizing Flexible Right Margin = 1 << 2,
UIView Autoresizing Flexible Top Margin = 1 << 3,
UIView Autoresizing Flexible Height = 1 << 4,
UIView Autoresizing Flexible Bottom Margin = 1 << 5
};
Swift imports this and other types defined using the NS_OPTIONS
macro
as a structure that conforms to the Option
protocol.
extension UIView {
struct Autoresizing Mask: Option Set {
init(raw Value: UInt)
static var flexible Left Margin: UIView.Autoresizing Mask
static var flexible Width: UIView.Autoresizing Mask
static var flexible Right Margin: UIView.Autoresizing Mask
static var flexible Top Margin: UIView.Autoresizing Mask
static var flexible Height: UIView.Autoresizing Mask
static var flexible Bottom Margin: UIView.Autoresizing Mask
}
}
At the time Option
was introduced (and Raw
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 Option
.
…but that’s getting ahead of ourselves.
This week on NSHipster,
let’s take a by-the-books look at using imported Option
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 Option
,
from ARHit
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.autoresizing Mask = .flexible Height
Option
conforms to the
Set
protocol,
so to you can specify multiple options with an array literal —
no bitwise operations required:
view.autoresizing Mask = [.flexible Height, .flexible Width]
To specify no options,
pass an empty array literal ([]
):
view.autoresizing Mask = [] // 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 Option
protocol
with a required raw
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: Option Set {
let raw Value: Int
static let pepperoni = Toppings(raw Value: 1 << 0)
static let onions = Toppings(raw Value: 1 << 1)
static let bacon = Toppings(raw Value: 1 << 2)
static let extra Cheese = Toppings(raw Value: 1 << 3)
static let green Peppers = Toppings(raw Value: 1 << 4)
static let pineapple = Toppings(raw Value: 1 << 5)
static let meat Lovers: Toppings = [.pepperoni, .bacon]
static let hawaiian: Toppings = [.pineapple, .bacon]
static let all: Toppings = [
.pepperoni, .onions, .bacon,
.extra Cheese, .green Peppers, .pineapple
]
}
Taken into a larger example for context:
struct Pizza {
enum Style {
case neapolitan, sicilian, new Haven, deep Dish
}
struct Toppings: Option Set { ... }
let diameter: Int
let style: Style
let toppings: Toppings
init(inches In Diameter diameter: Int,
style: Style,
toppings: Toppings = [])
{
self.diameter = diameter
self.style = style
self.toppings = toppings
}
}
let dinner = Pizza(inches In Diameter: 12,
style: .neapolitan,
toppings: [.green Peppers, .pineapple])
Another advantage of Option
conforming to Set
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 is Vegetarian: Bool {
return toppings.is Disjoint(with: [.pepperoni, .bacon])
}
}
dinner.is Vegetarian // true
A Fresh Take on an Old Classic
Alright, now that you know how to use Option
,
let’s show you how not to use Option
.
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 Raw
, Hashable
, and Case
.
protocol Option: Raw Representable, Hashable, Case Iterable {}
Next, declare an enumeration with String
raw values
that adopts the Option
protocol:
enum Topping: String, Option {
case pepperoni, onions, bacon,
extra Cheese, green Peppers, 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 Option
.
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, Case
makes it easy to order a pizza with “the works”:
extension Set where Element == Topping {
static var meat Lovers: Set<Topping> {
return [.pepperoni, .bacon]
}
static var hawaiian: Set<Topping> {
return [.pineapple, .bacon]
}
static var all: Set<Topping> {
return Set(Element.all Cases)
}
}
typealias Toppings = Set<Topping>
And that’s not all Case
has up its sleeves;
by enumerating over the all
type property,
we can automatically generate the bitset values for each case,
which we can combine to produce the equivalent raw
for any Set
containing Option
types:
extension Set where Element: Option {
var raw Value: Int {
var raw Value = 0
for (index, element) in Element.all Cases.enumerated() {
if self.contains(element) {
raw Value |= (1 << index)
}
}
return raw Value
}
}
Because Option
and Set
both conform to Set
our new Topping
implementation can be swapped in for the original one
without needing to change anything about the Pizza
itself.
You’re likely to encounter Option
when working with Apple SDKs in Swift.
And although you could create your own structure that conforms to Option
,
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.