Skip to content

3. Enums & pattern matching

What & why

A Swift enum defines a type with a fixed set of possible cases — but unlike enums in many languages, each case can carry its own data. Together with switch, this lets you make impossible states unrepresentable: the compiler checks that you handle every case.

Basic enums

enum Direction {
    case north, south, east, west
}

var heading = Direction.north
heading = .east                      // type is known, so the type name can be dropped

switch heading {
case .north: print("up")
case .south: print("down")
case .east, .west: print("sideways")
}

No default needed — the switch is exhaustive and the compiler knows all the cases. Add a new case later and every non-exhaustive switch becomes a compile error, pointing you to exactly what to update. That is a huge refactoring aid.

CaseIterable generates allCases:

enum Suit: CaseIterable {
    case hearts, diamonds, clubs, spades
}
print(Suit.allCases.count)           // 4
for suit in Suit.allCases { print(suit) }

Raw values

Give each case a fixed underlying value:

enum Planet: Int {
    case mercury = 1, venus, earth, mars    // venus = 2, earth = 3, ...
}
print(Planet.earth.rawValue)         // 3
print(Planet(rawValue: 4) as Any)    // Optional(mars) — failable init
enum Method: String {
    case get = "GET", post = "POST"
}
print(Method.post.rawValue)          // "POST"

String raw values default to the case name. Raw-value enums are automatically Equatable, Hashable, and (with Codable) easy to serialize.

Associated values

Each case can carry different data:

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(base: Double, height: Double)
}

func area(_ shape: Shape) -> Double {
    switch shape {
    case .circle(let r):
        return Double.pi * r * r
    case .rectangle(let w, let h):
        return w * h
    case .triangle(let b, let h):
        return 0.5 * b * h
    }
}

print(area(.rectangle(width: 3, height: 4)))   // 12.0

This is a sum type (or tagged union): a value is exactly one of the cases, with exactly the data that case needs. Compare that to a struct with radius, width, height, all optional — which allows nonsense combinations.

A realistic example:

enum LoadState {
    case idle
    case loading(progress: Double)
    case loaded(data: [String])
    case failed(message: String)
}

func describe(_ state: LoadState) -> String {
    switch state {
    case .idle:                       "Waiting"
    case .loading(let p):             "Loading \(Int(p * 100))%"
    case .loaded(let data):           "Got \(data.count) items"
    case .failed(let message):        "Error: \(message)"
    }
}

Pattern matching beyond switch

let state = LoadState.loaded(data: ["a", "b"])

// if case — match one pattern
if case .loaded(let items) = state {
    print(items.count)
}

// guard case
func items(from state: LoadState) -> [String] {
    guard case .loaded(let data) = state else { return [] }
    return data
}

// where clauses
switch state {
case .loading(let p) where p > 0.9: print("almost there")
case .loading:                      print("loading")
default:                            break
}

// for case — filter by pattern in a loop
let events: [LoadState] = [.idle, .failed(message: "x"), .failed(message: "y")]
for case .failed(let message) in events {
    print(message)                  // x, y
}

Other patterns work in switch: ranges (case 1...5), tuples, optionals (case let x?), type checks (case let s as String), and more.

Methods and properties on enums

enum Weekday: Int, CaseIterable {
    case monday = 1, tuesday, wednesday, thursday, friday, saturday, sunday

    var isWeekend: Bool { self == .saturday || self == .sunday }

    func next() -> Weekday {
        Weekday(rawValue: rawValue % 7 + 1)!
    }

    static var today: Weekday { .wednesday }     // illustration
}

print(Weekday.sunday.next())      // monday
print(Weekday.saturday.isWeekend) // true

Mutating methods can reassign self:

enum Light {
    case red, green, yellow
    mutating func advance() {
        switch self {
        case .red: self = .green
        case .green: self = .yellow
        case .yellow: self = .red
        }
    }
}

Optional and Result are enums

You've been using enums all along:

// Roughly how they're defined in the standard library:
// enum Optional<Wrapped> { case none; case some(Wrapped) }
// enum Result<Success, Failure: Error> { case success(Success); case failure(Failure) }

That's why switch over an optional works, and why you can write your own equivalents.

Recursive enums

Use indirect when a case contains the enum itself:

indirect enum Expr {
    case number(Double)
    case add(Expr, Expr)
    case multiply(Expr, Expr)
    case negate(Expr)
}

func evaluate(_ e: Expr) -> Double {
    switch e {
    case .number(let n):        return n
    case .add(let a, let b):    return evaluate(a) + evaluate(b)
    case .multiply(let a, let b): return evaluate(a) * evaluate(b)
    case .negate(let a):        return -evaluate(a)
    }
}

// (2 + 3) * -4
let expr = Expr.multiply(.add(.number(2), .number(3)), .negate(.number(4)))
print(evaluate(expr))      // -20.0

This is the classic way to model syntax trees, JSON values, file-system entries, and other tree data.

Namespacing with caseless enums

An enum with no cases can't be instantiated — a neat way to group constants and static functions:

enum Constants {
    static let apiVersion = "v2"
    static let timeout = 30.0
}

@unknown default for evolving enums

When you switch over an enum from another module that might gain cases later (like many Apple/Foundation enums), add @unknown default to get a warning when new cases appear while still compiling today:

switch someSystemEnum {
case .known: break
@unknown default: break
}

Common mistakes

  • Adding a default to your own enum's switch — it silences exactly the compiler help you want when cases are added. Only use it when truly appropriate.
  • Modeling variants as a struct of optionals instead of an enum with associated values.
  • Using raw values for everything. Raw values are for external representations (JSON, files); associated values model behaviour.
  • Forgetting indirect for recursive enums ("recursive enum is not marked 'indirect'").
  • Force-unwrapping init(rawValue:) on external input — handle nil.

Exercises

  1. Model a traffic Light enum with a computed duration: Int per state and a next property.
  2. Define enum PaymentMethod { case cash, card(last4: String), transfer(iban: String) } and write describe(_:).
  3. Build the Expr example further: add .divide, and make evaluate return Double? (division by zero → nil).
  4. Write func parse(_ s: String) -> Expr? for tiny inputs like "2+3" (single operator) — a warm-up for the capstone-style parsers.
  5. Use for case to count how many .failed events are in an array of LoadState.
  6. Create an enum Token (number(Int), plus, minus, leftParen, rightParen) and write a tokenizer from String to [Token].

Recap

  • Enums define closed sets of cases; switch over them is exhaustive.
  • Raw values map to external representations; associated values carry per-case data.
  • if case, guard case, and for case pattern-match without a full switch.
  • indirect enables recursive structures; Optional and Result are just enums.
  • Avoid default on your own enums so the compiler can help you refactor.

Next: Protocols →