Skip to content

Enumerations

In this chapter, you’ll continue to improve the implementation of Tic-Tac-Toe and discover a new category of types known as enumerations.

Enumerated types

Let’s revisit the type Game from the previous chapter. This type has a currentPlayer property that tracks whose turn it is:

swift
var currentPlayer: String

This property stores the current player’s symbol ("O" or "X"). However, because it’s a String, it can also store other values, not just "O" and "X" — this can be problematic.

For example, consider how you switch players after every turn:

swift
currentPlayer = currentPlayer == "O" ? "X" : "O"

Now consider what happens if you make a typo and set currentPlayer to "0" instead of "O":

swift
currentPlayer = currentPlayer == "O" ? "X" : "0"

Because your code assumes that currentPlayer can only be "O" or "X", this small typo stops your game from working correctly.

To protect against this problem, currentPlayer needs a more appropriate type than String. Its type should express that it can have only two possible values: O or X. Such a type is called an enumerated type, or enumeration.

Declaration

An enumeration is a type that has a predefined set of values. You declare an enumeration with the keyword enum. Inside the declaration, you enumerate the possible values with the case keyword:

swift
enum Player {
  
  case o
  case x
}

This declares an enumerated type Player. Every instance of Player must be either o or x; no other values are possible.

Enumeration cases

The possible values for an enumeration are known as its cases. Enumeration cases follow the same naming rules as variables, so they start with a lowercase letter.

You refer to the cases of Player as Player.o and Player.x:

swift
var currentPlayer: Player
currentPlayer = Player.o

If the compiler can infer the type of the enumeration, you can use the following shorthand:

swift
currentPlayer = .o

This is the same shorthand you used for static properties in Structures.

Type safety

Enumerations provide additional type safety. Now that currentPlayer is an enumeration instead of a String, you can rely on the compiler to check your code:

swift
currentPlayer = .O  

The compiler knows this code is invalid because O is not one of the cases of Player.

You can find other examples of enumerations in Blackjack. Here’s how the solution to that challenge stores a card’s rank and suit:

swift
typealias Card = (rank: String, suit: String)

As it turns out, this isn’t very safe. rank and suit accept any string, so you need to remember whether you store hearts as "hearts", "Hearts", or "♥️", and then not make any mistakes.

The possible values for rank and suit are enumerable, so you can create the following types:

swift
enum Rank {

  case ace
  case two
  case three
  case four
  case five
  case six
  case seven
  case eight
  case nine
  case ten 
  case jack
  case queen
  case king
}

enum Suit {

  case hearts
  case diamonds
  case spades
  case clubs
}

By using these enumerations instead of String, you no longer need to remember which values you’re using; the compiler checks your code and reports any mistakes:

swift
typealias Card = (rank: Rank, suit: Suit)

var card: Card
card = (.ace, .spades)
card = (.two, .pikes)  

Control flow with cases

After you’ve declared a variable of an enumeration type, you can use a switch statement or expression to find out which value it contains:

swift
func symbol(for player: Player) -> String {
  switch player {
  case .o: "O"
  case .x: "X"
  }
}

This function returns the correct symbol to print for each player.

The switch expression in this example is exhaustive. It doesn’t require a default case because the compiler knows that player can only be o or x.

Of course, if you only want to check for a specific case, you can use the equality operator (==):

swift
if currentPlayer == .o {
  currentPlayer = .x
} else {
  currentPlayer = .o
}

Properties, methods, and initializers

Enumerations and structures have a lot in common. Both are types with properties, methods, and initializers. However, unlike structures, enumerations cannot have stored properties. Instead of properties, you use cases to store the state of an enumeration.

To understand how this works, consider the following types:

swift
struct Point {

  var x: Double
  var y: Double
}

enum Result {

  case success
  case failure
}

Here, the state of a Point consists of a value for x and a value for y, whereas the state of a Result is either success or failure. In other words, the state of a structure is a combination of all of its properties, whereas the state of an enumeration is a selection of one of its cases.

Enumerations can have computed properties, as these don’t store state. To implement a computed property, you can compare the value of self against the possible cases:

swift
enum Player {
  // ...

  var symbol: String {
    switch self {
    case .o: "O"
    case .x: "X"
    }
  }
}

The same is true for methods:

swift
enum Player {
  // ...

  func next() -> Player {
    self == .o ? .x : .o
  }
}

You can create a mutating method by assigning a different case to self:

swift
enum Player {
  // ...

  mutating func toggle() {
    if self == .o {
      self = .x
    } else {
      self = .o
    }
  }
}

This is also how you implement initializers. For example, here’s how you can initialize a player from their symbol:

swift
enum Player {
  // ...

  init?(symbol: String) {
    switch symbol {
    case "O":
      self = .o
    case "X":
      self = .x
    default:
      return nil
    }
  }
}

This initializer is failable and returns nil if you pass in a symbol other than "O" or "X".

Finally, enumerations can also have static members. Here’s a static method that returns a random player:

swift
enum Player {
  // ...

  static func random() -> Player {
    Bool.random() ? .o : .x
  }
}

Raw values

Enumerations often replace types such as String or Int for values that can be enumerated. Unfortunately, you can’t always fully replace these types. For example, the cases of type Player replace the strings "O" and "X", but you still need these strings to print the board.

In situations like this, it’s helpful to declare an enumeration with a raw type. Here’s how you declare that Player has a raw type of String:

swift
enum Player: String {
  
  case o
  case x
}

When an enumeration has a raw type, every enumeration case has an equivalent raw value of this type.

In the previous example, the raw values for o and x are "o" and "x" — the name of each case. You can override these defaults by assigning explicit raw values:

swift
enum Player: String {
  
  case o = "O"
  case x = "X"
}

An enumeration with a raw type receives a rawValue property and an init?(rawValue:) initializer that you can use to convert between an enumeration case and its raw value:

swift
currentPlayer.rawValue
currentPlayer = Player(rawValue: "X")!

This combination of a property and a failable initializer is similar to the symbol property and init?(symbol:) initializer from earlier. You can remove these now that you have a raw type.

Other than String, the types you can use for raw values are Character, Int, Double, and other number types.

For type Int, the default raw values start at 0 and increment from one case to the next. If you assign some of the cases an explicit raw value, the compiler will increment from these values instead.

For example:

swift
enum HTTPStatus: Int {

  case ok = 200
  case created
  case noContent = 204

  case moved = 301
  case found

  case badRequest = 400
  case unauthorized
  case forbidden = 403
  case notFound
}

Here, the compiler assigns the following raw values:

  • created: 201
  • found: 302
  • unauthorized: 401
  • notFound: 404

Raw values create a one-to-one mapping between an enumeration case and its equivalent raw value. Therefore, you cannot use raw values to map a card’s rank to its value in a game of Blackjack:

swift
enum Rank: Int {

  case ace = 1
  case two
  case three
  case four
  case five
  case six
  case seven
  case eight
  case nine
  case ten 
  case jack = 10
  case queen = 10
  case king = 10
}

This code is invalid because cases ten, jack, queen, and king all have the same raw value.

You can implement a rank’s value as a computed property instead. You’ll do this in an upcoming exercise.

Caseless enumerations

In the previous chapter, you declared a type Input and gave it a private initializer to prevent it from being instantiated:

swift
struct Input {

  private init() { }

  static func readPosition(on board: Board) -> Int {
    // ...
  }

  static func readBool(question: String) -> Bool {
    // ...
  }
}

You can achieve the same result with an enumeration:

swift
enum Input {

  static func readPosition(on board: Board) -> Int {
    // ...
  }

  static func readBool(question: String) -> Bool {
    // ...
  }
}

As you can see, you can declare an enumeration without any cases. You cannot instantiate this enumeration because every instance must equal one of its cases, of which there are none.

A caseless enumeration is the best way to declare a type that shouldn’t be instantiated.

Up next

This chapter introduced a new category of types called enumerations. In the upcoming exercises, you’ll use enumerations to improve your implementation of Blackjack.

To prepare for these exercises, first study the solution project for this chapter. This project uses the Player and Input enumerations introduced here to improve the implementation of Tic-Tac-Toe.

After you’ve completed the exercises, continue on to the next chapter, where you’ll learn about classes and how they differ from structures and enumerations.

You’ll also revisit enumerations in Wrapping Up to learn about their advanced features.