Quantcast
Channel: NSHipster
Viewing all articles
Browse latest Browse all 382

OptionSet

$
0
0

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:

typedefNS_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.

extensionUIView{structAutoresizingMask:OptionSet{init(rawValue:UInt)staticvarflexibleLeftMargin:UIView.AutoresizingMaskstaticvarflexibleWidth:UIView.AutoresizingMaskstaticvarflexibleRightMargin:UIView.AutoresizingMaskstaticvarflexibleTopMargin:UIView.AutoresizingMaskstaticvarflexibleHeight:UIView.AutoresizingMaskstaticvarflexibleBottomMargin:UIView.AutoresizingMask}}

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:

structToppings:OptionSet{letrawValue:Intstaticletpepperoni=Toppings(rawValue:1<<0)staticletonions=Toppings(rawValue:1<<1)staticletbacon=Toppings(rawValue:1<<2)staticletextraCheese=Toppings(rawValue:1<<3)staticletgreenPeppers=Toppings(rawValue:1<<4)staticletpineapple=Toppings(rawValue:1<<5)staticletmeatLovers:Toppings=[.pepperoni,.bacon]staticlethawaiian:Toppings=[.pineapple,.bacon]staticletall:Toppings=[.pepperoni,.onions,.bacon,.extraCheese,.greenPeppers,.pineapple]}

Taken into a larger example for context:

structPizza{enumStyle{caseneapolitan,sicilian,newHaven,deepDish}structToppings:OptionSet{...}letdiameter:Intletstyle:Stylelettoppings:Toppingsinit(inchesInDiameterdiameter:Int,style:Style,toppings:Toppings=[]){self.diameter=diameterself.style=styleself.toppings=toppings}}letdinner=Pizza(inchesInDiameter:12,style:.neapolitan,toppings:[.greenPepper,.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:

extensionPizza{varisVegetarian:Bool{returntoppings.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 cakepizza 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:

enumTopping:String,Option{casepepperoni,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”:

extensionSetwhereElement==Topping{staticvarmeatLovers:Set<Topping>{return[.pepperoni,.bacon]}staticvarhawaiian:Set<Topping>{return[.pineapple,.bacon]}staticvarall:Set<Topping>{returnSet(Element.allCases)}}typealiasToppings=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:

extensionSetwhereElement:Option{varrawValue:Int{varrawValue=0for(index,element)inElement.allCases.enumerated(){ifself.contains(element){rawValue|=(1<<index)}}returnrawValue}}

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.


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.


Viewing all articles
Browse latest Browse all 382

Trending Articles