Enumerations in Swift are first-class types the same as Structs and Classes. This means they can have instance methods and initializers and even associated values making them much more powerful than plain old Objective-C enumerations. If you assign raw values to a Swift enum the compiler will even generate a failable initializer for you. This post looks at a practical example of why that is useful and how you can add your own failable initializers.
A Practical Example
Let’s look at a common situation where we might use a Swift enum. Suppose I want to use a UISegmentedControl
to choose an item from a set of values. In this case an interval of time for a repeating schedule:
I can use a Swift enum to model the possible choices:
enum UYLNotificationRepeatInterval {
case none
case minute
case hour
case day
case week
}
It is then not uncommon to see a method in the view controller with a big switch
to read the segment control:
// Assuming scheduleControl is of type UISegmentedControl
var interval = UYLNotificationRepeatInterval.none
switch scheduleControl.selectedSegmentIndex {
case 0: interval = .none
case 1: interval = .minute
case 2: interval = .hour
case 3: interval = .week
default: interval = .none
}
This type of code can quickly clutter a view controller.
Raw Values and Failable Initializers
Each case of a Swift enum can be assigned a raw value. The raw values all have to be of the same type but you can choose between using a string, characters or any of the integer or floating point types.
To make life easier if you choose either integer or string raw values the compiler will automatically assign default values. For integers the compiler assigns a value which is one more than the last case. If you don’t assign a value for the first case it defaults to zero. You can mix default with explicit assignments:
enum OrderCodes: Int {
case none
case low
case high = 100
case rush
}
An enum that has a raw value type also gets a rawValue
property which returns the raw value. So for the above example:
OrderCodes.none.rawValue // 0
OrderCodes.low.rawValue // 1
OrderCodes.high.rawValue // 100
OrderCodes.rush.rawValue // 101
If you use a String
the default value is simply the case name:
enum Name: String {
case fred
case joseph = "joe"
}
Name.fred.rawValue // fred
Name.joseph.rawValue // joe
What starts to make this useful is that the Swift compiler gives you a failable initializer for the rawValue
for free. A failable initializer returns an optional that will be nil
if we try to initialize with an invalid raw value. This means you can write things like this:
if let magic = MagicValue(rawValue: someValue) {
print("got \(magic)")
}
This gives us a shortcut for our earlier segmented control code if we organize our raw values to match the index of our segmented control. We can skip the switch
and do this:
// Assuming our enum now has a raw value of type Int
// enum UYLNotificationRepeatInterval: Int {...}
if let interval = UYLNotificationRepeatInterval(rawValue: scheduleControl.selectedSegmentIndex) {
// do something with the enum
}
Adding an Initializer
The last example worked because I carefully arranged the raw values of the enum to match the index positions of the segmented control. What if that is not possible. What if I want my rawValue to be a String
or some other arbitrary integer value that cannot match the segment index values.
The solution is to add our own failable initializer(s) to the enum. For the sake of this example let’s switch our enum to use a String
as the raw value type:
enum UYLNotificationRepeatInterval: String {
case none
case minute
case hour
case day
case week
}
We still get our free failable initializer from the compiler but it now takes a String
as the argument:
if let week = UYLNotificationRepeatInterval(rawValue: "week") {
print("got a week")
}
We need to be able to initialize our type from the Int
value returned by the segmented control. So we need an initializer that takes an Int
and returns an optional type. The complete enum code looks like this:
enum UYLNotificationRepeatInterval: String {
case none
case minute
case hour
case day
case week
init?(index: Int) {
switch index {
case 0: self = .none
case 1: self = .minute
case 2: self = .hour
case 3: self = .day
case 4: self = .week
default: return nil
}
}
}
The code to create an interval from the segment control now becomes:
if let interval = UYLNotificationRepeatInterval(index:
scheduleControl.selectedSegmentIndex) {
// do something with the enum
}
This still has some weakness in that it relies on the configuration of the segment control matching the switch in the initializer but that is a problem for another day.