Home iOS & Swift Books Swift Apprentice

15
Enumerations Written by Ben Morrow

One day in your life as a developer, you’ll realize you’re being held captive by your laptop. Determined to break from convention, you’ll decide to set off on a long trek by foot. Of course, you’ll need a map of the terrain you’ll encounter. Since it’s the 21st century, and you’re fluent in Swift, you’ll complete one final project: a custom map app.

As you code away, you think it would be swell if you could represent the cardinal directions as variables: north, south, east, west. But what’s the best way to do this in code?

You could represent each value as an integer, like so:

  • North: 1
  • South: 2
  • East: 3
  • West: 4

You can see how this could quickly get confusing if you or your users happen to think of the directions in a different order. “What does 3 mean again?” To alleviate that, you might represent the values as strings, like so:

  • North: "north"
  • South: "south"
  • East: "east"
  • West: "west"

The trouble with strings, though, is that the value can be any string. What would your app do if it received "up" instead of "north"? Furthermore, it’s all too easy to make a typo like "nrth".

Wouldn’t it be great if there were a way to create a group of related, compiler-checked values? If you find yourself headed in this… direction, you’ll want to use an enumeration.

An enumeration is a list of related values that define a common type and let you work with values in a type-safe way. The compiler will catch your mistake if your code expects a Direction and you try to pass in a float like 10.7 or a misspelled direction like "Souuth".

Besides cardinal directions, other good examples of related values are colors (black, red, blue), card suits (hearts, spades, clubs, diamonds) and roles (administrator, editor, reader).

Enumerations in Swift are more powerful than they are in other languages such as C or Objective-C. They share features with the structure and class types you learned about in the previous chapters. An enumeration can have methods and computed properties, all while acting as a convenient state machine.

In this chapter, you’ll learn how enumerations work and when they’re useful. As a bonus, you’ll finally discover what an optional is under the hood. Hint: They are implemented with enumerations!

Your first enumeration

Your challenge: construct a function that will determine the school semester based on the month. One way to solve this would be to use an array of strings and match the semesters with a switch statement:

let months = ["January", "February", "March", "April", "May",
              "June", "July", "August", "September", "October",
              "November", "December"]

func semester(for month: String) -> String {
  switch month {
  case "August", "September", "October", "November", "December":
    return "Autumn"
  case "January", "February", "March", "April", "May":
    return "Spring"
  default:
    return "Not in the school year"
  }
}

semester(for: "April") // Spring

Running this code in a playground, you can see that the function correctly returns "Spring". But as I mentioned in the introduction, you could easily mistype a string. A better way to tackle this would be with an enumeration.

Declaring an enumeration

To declare an enumeration, you list out all the possible member values as case clauses:

enum Month {
  case january
  case february
  case march
  case april
  case may
  case june
  case july
  case august
  case september
  case october
  case november
  case december
}
enum Month {
  case january, february, march, april, may, june, july, august,
  september, october, november, december
}

Deciphering an enumeration in a function

You can rewrite the function that determines the semester so that it uses enumeration values instead of string-matching.

func semester(for month: Month) -> String {
  switch month {
  case Month.august, Month.september, Month.october,
       Month.november, Month.december:
    return "Autumn"
  case Month.january, Month.february, Month.march, Month.april,
       Month.may:
    return "Spring"
  default:
    return "Not in the school year"
  }
}
func semester(for month: Month) -> String {
  switch month {
  case .august, .september, .october, .november, .december:
    return "Autumn"
  case .january, .february, .march, .april, .may:
    return "Spring"
  default:
    return "Not in the school year"
  }
}
func semester(for month: Month) -> String {
  switch month {
  case .august, .september, .october, .november, .december:
    return "Autumn"
  case .january, .february, .march, .april, .may:
    return "Spring"
  case .june, .july:
    return "Summer"
  }
}
var month = Month.april
semester(for: month) // "Spring"

month = .september
semester(for: month) // "Autumn"

Mini-exercise

Wouldn’t it be nice to request the semester from an instance like month.semester instead of using the function? Add a semester computed property to the month enumeration so that you can run this code:

let semester = month.semester // "Autumn"

Code completion prevents typos

Another advantage of using enumerations instead of strings is that you’ll never have a typo in your member values. Xcode provides code completion:

Raw values

Unlike enumeration values in C, Swift enum values are not backed by integers as a default. That means january is itself the value.

enum Month: Int {
enum Month: Int {
  case january = 1, february = 2, march = 3, april = 4, may = 5,
  june = 6, july = 7, august = 8, september = 9,
  october = 10, november = 11, december = 12
}
enum Month: Int {
  case january = 1, february, march, april, may, june, july,
  august, september, october, november, december
}

Accessing the raw value

Enumeration instances with raw values have a handy rawValue property. With the raw values in place, your enumeration has a sense of order, and you can calculate the number of months left until winter break:

func monthsUntilWinterBreak(from month: Month) -> Int {
  Month.december.rawValue - month.rawValue
}
monthsUntilWinterBreak(from: .april) // 8

Initializing with the raw value

You can use the raw value to instantiate an enumeration value with an initializer. You can use init(rawValue:) to do this, but if you try to use the value afterward, you’ll get an error:

let fifthMonth = Month(rawValue: 5)
monthsUntilWinterBreak(from: fifthMonth) // Error: not unwrapped
let fifthMonth = Month(rawValue: 5)!
monthsUntilWinterBreak(from: fifthMonth) // 7

Mini-exercise

Make monthsUntilWinterBreak a computed property of the Month enumeration, so that you can execute the following code:

let monthsLeft = fifthMonth.monthsUntilWinterBreak // 7

String raw values

Similar to the handy trick of incrementing an Int raw value, if you specify a raw value type of String you’ll get another automatic conversion. Let’s pretend you’re building a news app that has tabs for each section. Each section has an icon. Icons are a good opportunity to deploy enumerations because, by their nature, they are a limited set:

// 1
enum Icon: String {
  case music
  case sports
  case weather

  var filename: String {
    // 2
    "\(rawValue).png"
  }
}
let icon = Icon.weather
icon.filename // weather.png

Unordered raw values

Integer raw values don’t have to be in an incremental order. Coins are a good use case:

enum Coin: Int {
  case penny = 1
  case nickel = 5
  case dime = 10
  case quarter = 25
}

let coin = Coin.quarter
coin.rawValue // 25

Mini-exercise

Create an array called coinPurse that contains coins. Add an assortment of pennies, nickels, dimes and quarters to it.

Associated values

Associated values take Swift enumerations to the next level in expressive power. They let you associate a custom value (or values) with each enumeration case.

var balance = 100

func withdraw(amount: Int) {
  balance -= amount
}
enum WithdrawalResult {
  case success(newBalance: Int)
  case error(message: String)
}
func withdraw(amount: Int) -> WithdrawalResult {
  if amount <= balance {
    balance -= amount
    return .success(newBalance: balance)
  } else {
    return .error(message: "Not enough money!")
  }
}
let result = withdraw(amount: 99)

switch result {
case .success(let newBalance):
  print("Your new balance is: \(newBalance)")
case .error(let message):
  print(message)
}
enum HTTPMethod {
  case get
  case post(body: String)
}
let request = HTTPMethod.post(body: "Hi there")
guard case .post(let body) = request else {
  fatalError("No message was posted")
}
print(body)

Enumeration as state machine

An enumeration is an example of a state machine, meaning it can only ever be a single enumeration value at a time, never more. The friendly traffic light illustrates this concept well:

enum TrafficLight {
  case red, yellow, green
}
let trafficLight = TrafficLight.red

Mini-exercise

A household light switch is another example of a state machine. Create an enumeration for a light that can switch .on and .off.

Iterating through all cases

Sometimes you want to loop through all of the cases in an enumeration. This is easy to do:

enum Pet: CaseIterable {
  case cat, dog, bird, turtle, fish, hamster
}

for pet in Pet.allCases {
  print(pet)
}
cat
dog
bird
turtle
fish
hamster

Enumerations without any cases

In Chapter 12, “Methods” you learned how to create a namespace for a group of related type methods. The example in that chapter looked like this:

struct Math {
  static func factorial(of number: Int) -> Int {
    (1...number).reduce(1, *)
  }
}
let factorial = Math.factorial(of: 6) // 720
let math = Math()
enum Math {
  static func factorial(of number: Int) -> Int {
    (1...number).reduce(1, *)
  }
}
let factorial = Math.factorial(of: 6) // 720
let math = Math() // ERROR: No accessible initializers

Mini-exercise

Euler’s number is useful in calculations for statistical bell curves and compound growth rates. Add the constant e, 2.7183, to your Math namespace. Then you can figure out how much money you’ll have if you invest $25,000 at 7% continuous interest for 20 years:

let nestEgg = 25000 * pow(Math.e, 0.07 * 20) // $101,380.95

Optionals

Since you’ve made it through the lesson on enumerations, the time has come to let you in on a little secret. There’s a Swift language feature using enumerations right under your nose all along: optionals! In this section, you’ll explore their underlying mechanism.

var age: Int?
age = 17
age = nil
switch age {
case .none:
  print("No value")
case .some(let value):
  print("Got a value: \(value)")
}
let optionalNil: Int? = .none
optionalNil == nil    // true
optionalNil == .none  // true

Challenges

Before moving on, here are some challenges to test your knowledge of enumerations. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Adding raw values

Take the coin example from earlier in the chapter then begin with the following array of coins:

enum Coin: Int {
  case penny = 1
  case nickel = 5
  case dime = 10
  case quarter = 25
}

let coinPurse: [Coin] = [.penny, .quarter, .nickel, .dime, .penny, .dime, .quarter]

Challenge 2: Computing with raw values

Take the example from earlier in the chapter and begin with the Month enumeration:

enum Month: Int {
  case january = 1, february, march, april, may, june, july,
       august, september, october, november, december
}

Challenge 3: Pattern matching enumeration values

Take the map example from earlier in the chapter and begin with the Direction enumeration:

enum Direction {
  case north
  case south
  case east
  case west
}
let movements: [Direction] = [.north, .north, .west, .south,
  .west, .south, .south, .east, .east, .south, .east]
var location = (x: 0, y: 0)

Key points

  • An enumeration is a list of mutually exclusive cases that define a common type.
  • Enumerations provide a type-safe alternative to old-fashioned integer values or strings.
  • You can use enumerations to handle responses, store state and encapsulate values.
  • CaseIterable lets you loop through an enumeration with allCases.
  • Uninhabited enumerations can be used as namespaces and prevent the creation of instances.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.