Enum Raw Values and Failable Initializers

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:

Segmented control

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.

Further Reading