Exploring Swift 2.0 OptionSetTypes
/OptionSetType is a protocol that intends to modernise the methodologies used for single variable option masks. Traditionally this has been done with bit masks, and lots of logical &s and |s and replaces them with explicit set syntax, with appropriate methods for checking and combining OptionSets
Let's look at a worked example. I am currently writing a series of posts about GameplayKit, and one of things I need to represent constantly is data about directions. There a couple of uses, control system as well as accessibility in the map. Let's consider a joystick example. Joysticks or digital pads typically had 4 direction switches, one for up, down, left and right. Horizontal and vertical axis can be active at the same time (e.g. up and left). Sounds like a perfect candidate for an OptionSetType.
public struct Directions : OptionSetType{ public let rawValue : Int public init(rawValue:Int){ self.rawValue = rawValue} static let Up = Directions(rawValue:1) static let Down = Directions(rawValue:2) static let Left = Directions(rawValue:4) static let Right = Directions(rawValue:8) }
Fundamentally, that's all that's needed. I can now create instances of them and test to see if the player is pressing Up
var joystickDirection = Directions.Up if joystickDirection == Directions.Up{ //Move up }
I can also represent a joystick that is both Up and Left.
var joystickDirection : Directions = [Directions.Up,Directions.Left] if joystickDirection.contains(Directions.Up) { //Move up }
The contains method returns true if the supplied value is in the set. We might want to allow user's to take advantage of some predefined combinations (as they are meaningful), we can do this by adding the following to our constants in the Directions class
static let DiagonalUpLeft : Directions = [Up,Left] static let DiagonalUpRight : Directions = [Up,Right] static let DiagonalDownLeft : Directions = [Down,Left] static let DiagonalDownRight : Directions = [Down, Right]
Note that we are using set syntax to define the options, and therefore it's important to annotate the type (DiagonalUpLeft : Directions = [Up,Left]) otherwise Swift will just try and infer the type and you won't get your OptionSetType. We had to do this in our second example as well.
Now contains would be true for both Up and Left, and we could explicitly check for a the diagonal with
var joystickDirection : Directions = [Directions.Up,Directions.Left] if joystickDirection == Directions.DiagonalUpLeft { //Move up and left }
Very neat! Now there are a couple of things that bother me at this point... the first is we have magic numbers: 1,2,4,8. They are reasonably constrained but there's a second problem that bothers me... The playground output is just
Not super useful. I can't see what directions are in the set. Now, I could just support the CustomStringConvertible protocol, but it gets a little complicated. I need to know what all the values are AND then map them to strings. I'd really like to encapsulate the two ideas. So let's embed an enum that captures a unary Direction.
private enum Direction : Int,CustomStringConvertible { case Up=1, Down=2, Left=4, Right=8 var description : String { var shift = 0 while (rawValue >> shift != 1){ shift++ } return ["Up","Down","Left","Right"][shift] } }
Now we can refer to the various cases of the enum, rather than the magic numbers. We can also provide a simple way of getting the name of each case.
This enables us to make the rest of the class a little clearer too
public struct Directions : OptionSetType{//, CustomStringConvertible{ private enum Direction : Int,CustomStringConvertible { case Up=1, Down=2, Left=4, Right=8 var description : String { var shift = 0 while (rawValue >> shift != 1){ shift++ } return ["Up","Down","Left","Right"][shift] } } public let rawValue : Int public init(rawValue:Int){ self.rawValue = rawValue} private init(_ direction:Direction){ self.rawValue = direction.rawValue } static let Up = Directions(Direction.Up) static let Down = Directions(Direction.Down) static let Left = Directions(Direction.Left) static let Right = Directions(Direction.Right) static let DiagonalUpLeft : Directions = [Up,Left] static let DiagonalUpRight : Directions = [Up,Right] static let DiagonalDownLeft : Directions = [Down,Left] static let DiagonalDownRight : Directions = [Down, Right] }
Much more explicit. We've also created the possibility that we could use the same mask in other cases where we need to represent direction (say axis where [Left,Right] would be valid).
Now we can also provide an implementation of description that can be ignorant of how many directions there are
public var description : String{ var result = "" var shift = 0 while let currentDirection = Direction(rawValue: 1 << shift++){ if self.contains(Directions(currentDirection)){ result += (result.characters.count == 0) ? "\(currentDirection)" : ",\(currentDirection)" } } return "[\(result)]" }
We just keep shifting 1 by more. If we wanted to add diagonals as explicit directions we could add them our enum, add the cases to the OptionSet, and other code (like description) would just keep working for the additional cases.
The output is now much more useful
OptionSetTypes are very powerful and expressive, and I encourage you to go and play for yourself. You know. In a playground.