Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

16. Enumerations
Written by Ehab Amer

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

As you code away, you think it would be swell to represent the cardinal directions as variables: north, south, east and 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

This encoding 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 Chapter 11, “Structures”, and Chapter 14, “Classes”. An enumeration can have methods and computed properties while holding a particular state.

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 to 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 you saw in the introduction, it’s easy to 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 to use 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 "Not in the school year"
  }
}
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"

Using Code Completion to Prevent 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)! // may
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. 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

let aSmallCoin = Coin.dime
coin.rawValue > aSmallCoin.rawValue   //true

aSmallCoin.rawValue + coin.rawValue   //35

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 a State Machine

An enumeration is an example of a state machine, meaning it can only ever be a single case 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 13, “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

Mini-Exercise

Euler’s number is useful in calculating 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 this far, the time has come to let you in on a little secret. There’s a Swift language feature that’s been 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 and 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.
  • The Swift Optional type is a generic enumeration with cases .none and .some.
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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now