Modelling with types · core

Enums and associated values

Swift's enums carry data, which makes them the tool for stopping illegal states from being representable at all.

12 min read15 min practice0/3 exercises5 recall cards

By the end you will be able to

  • Define enums with raw values, associated values, methods and computed properties
  • Model a state machine so that impossible combinations cannot be written
  • Use CaseIterable and failable raw-value initializers
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

Which of these can a Swift enum case do that a C enum case cannot?

The familiar part

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

let heading = Direction.north
let other: Direction = .south      // type known, so `.south` is enough

.south without the type name works whenever Swift already knows what type is expected. You will see this everywhere in SwiftUI.

Raw values

Give the enum a raw type and each case gets a fixed underlying value:

enum HTTPStatus: Int {
    case ok = 200
    case notFound = 404
    case serverError = 500
}

HTTPStatus.ok.rawValue              // 200
HTTPStatus(rawValue: 404)           // Optional(.notFound)
HTTPStatus(rawValue: 999)           // nil — no case matches

The initializer is failable — it returns an optional — because arbitrary input might not correspond to any case. That is exactly the right shape for parsing external data.

For String raw values, the case name is used by default:

enum Theme: String {
    case light, dark, system     // rawValues "light", "dark", "system"
}
Swift

CaseIterable gives you allCases for free — useful for pickers, menus and tests.

Associated values: the important part

Each case can carry its own data, of its own type:

enum Measurement {
    case metres(Double)
    case feetAndInches(feet: Int, inches: Int)
    case unknown
}

You pull the data back out by pattern matching:

switch measurement {
case .metres(let m):
    print("\(m) m")
case .feetAndInches(let feet, let inches):
    print("\(feet)ft \(inches)in")
case .unknown:
    print("no measurement")
}

This is the feature that makes enums a modelling tool rather than a naming convention.

Swift

Making illegal states unrepresentable

This is the single most valuable idea in this lesson.

Here is a screen state modelled with booleans:

struct ScreenState {
    var isLoading: Bool
    var items: [Item]
    var errorMessage: String?
}

Three booleans-worth of combinations — eight states, most of them nonsense. What does it mean to be loading and have an error? To have items and an error? Every one of those combinations is something a reader has to consider, and something a bug can produce.

Now the same thing as an enum:

enum ScreenState {
    case loading
    case loaded([Item])
    case failed(String)
    case empty
}

There are exactly four states. "Loading with an error" cannot be written down. And because switch must be exhaustive, every place that renders this state is forced to say what each case looks like.

Booleans: 8 combinations, 4 of them meaningless
if state.isLoading {
    showSpinner()
} else if let error = state.errorMessage {
    showError(error)
} else if state.items.isEmpty {
    showEmpty()
} else {
    show(state.items)
}
// What if isLoading AND errorMessage?
// The order of these branches is
// silently load-bearing.
Enum: exactly 4 states, all meaningful
switch state {
case .loading:
    showSpinner()
case .failed(let message):
    showError(message)
case .empty:
    showEmpty()
case .loaded(let items):
    show(items)
}
// No ordering subtleties.
// No impossible combinations.
// Adding a case breaks the build
// exactly where it must.

This is the pattern behind almost every well-built SwiftUI screen. You will meet it again when you get to state and data flow.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

Enums can have methods and recursion. What does this print?

indirect enum Expression {
    case number(Int)
    case add(Expression, Expression)
    case multiply(Expression, Expression)

    func evaluate() -> Int {
        switch self {
        case .number(let value):
            return value
        case .add(let left, let right):
            return left.evaluate() + right.evaluate()
        case .multiply(let left, let right):
            return left.evaluate() * right.evaluate()
        }
    }
}

let tree = Expression.multiply(
    .add(.number(2), .number(3)),
    .number(4)
)

print(tree.evaluate())

Enums can have methods, computed properties and initializers

Everything you can put on a struct, except stored properties. An enum's storage is its case and payload.

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

    var isStartOfWeek: Bool { self == .monday }

    func next() -> Weekday {
        Weekday(rawValue: rawValue % 5 + 1) ?? .monday
    }
}

Integer raw values auto-increment from the last one you specify, so tuesday is 2 and friday is 5.

Check yourself

Why can an enum not have stored properties?

Model a network result

Write the code

Define a FetchResult enum with three cases — success carrying a [String], failure carrying a message, and cancelled — then print a description of each.

Solution unlocks after 3 attempts

Parse raw values safely

Write the code

Convert a list of strings into Theme values, skipping anything that does not match a case, and print what survived.

Solution unlocks after 3 attempts

Replace the booleans

Write the code

Rewrite this state as an enum so that impossible combinations cannot be expressed, then render each state.

Solution unlocks after 3 attempts
Design itSaved on this device. Never graded.

Take a feature you have used recently — a login screen, a video player, a checkout flow — and write down its states as a Swift enum with associated values. Aim for the smallest set of cases where every case is meaningful and none of them overlap.

Checkpoint

You can now:

  • Define enums with raw values, associated values, methods and computed properties
  • Use CaseIterable and failable raw-value initializers
  • Recognise a boolean soup and replace it with an enum
  • Explain when indirect is needed

Next up: protocols — describing what a type can do rather than what it is, and the foundation of everything SwiftUI does.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.