EduFlow

Section 2 · Modelling with Types

Enums with Associated Values

16 min3 min readFill in the blanks

Lesson at a glance

Replace fragile strings with enums, attach data to each case with associated values, and let exhaustive switches find every place a new case needs handling.

  1. 1Enums with raw values
  2. 2Exhaustive switches are a feature
  3. 3Associated values: cases that carry data
  4. 4Getting the data back out
  5. 5Enums instead of boolean soup

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

In the last section, an expense’s category was a String. That works until someone writes "Fod", or "food" in one place and "Food" in another, and the monthly total silently misses half your groceries. Nothing in the type said which strings were valid. An enum says exactly that: here is the complete list of possible values, and nothing else is allowed.

Enums with raw values

Category.swift
enum Category: String, CaseIterable {
    case food
    case transport
    case housing
    case fun

    var title: String {
        rawValue.capitalized
    }

    var symbolName: String {
        switch self {
        case .food: "fork.knife"
        case .transport: "bus"
        case .housing: "house"
        case .fun: "ticket"
        }
    }
}

Category now has exactly four values. Write .fod and the build fails. The : String gives each case a raw value, its name as a string by default (Category.food.rawValue is "food"), which is what you’ll save to disk and read from JSON later. Going the other way is failable, because not every string is a category: Category(rawValue: "fun") is .fun, and Category(rawValue: "Fun") is nil.

CaseIterable asks the compiler to generate Category.allCases, an array of every case in declaration order. Pocket Budget’s category picker will loop over it, so adding a case adds a row to the picker for free.

Enums can have computed properties and methods, just like structs. symbolName maps each case to an SF Symbols icon name, and the switch inside it has no default.

Exhaustive switches are a feature

Suppose you add case gifts next month. The build immediately fails at symbolName with “switch must be exhaustive”, and at every other switch over Category in the project. The compiler has produced your to-do list. Compare that with the string version, where a new category quietly falls into whatever default branch existed and shows the wrong icon in production.

Associated values: cases that carry data

Raw values are fixed per case. Some data varies per instance. Pocket Budget records how an expense was paid, and each method needs different details:

PaymentMethod.swift
enum PaymentMethod {
    case cash
    case card(lastFour: String)
    case transfer(bank: String, reference: String?)
}

let lunch: PaymentMethod = .cash
let groceries: PaymentMethod = .card(lastFour: "4242")
let rent: PaymentMethod = .transfer(bank: "Monzo", reference: "RENT-10")

Each case has its own shape. A cash payment carries nothing, a card payment carries four digits, and a transfer carries a bank and an optional reference. A struct would need three optional properties and a comment explaining which ones are filled in when. The enum makes invalid combinations, such as a cash payment with a bank name, impossible to construct.

The PaymentMethod enum branches into three cases: cash with no data, card carrying lastFour, and transfer carrying bank and an optional reference. PaymentMethod .cash no data .card lastFour .transfer bank, reference?
One enum, three differently shaped cases

Getting the data back out

You read associated values with pattern matching in a switch:

Swift
func label(for method: PaymentMethod) -> String {
    switch method {
    case .cash:
        return "Cash"
    case .card(let lastFour):
        return "Card ending \(lastFour)"
    case .transfer(let bank, let reference?):
        return "\(bank) transfer, ref \(reference)"
    case .transfer(let bank, nil):
        return "\(bank) transfer"
    }
}

label(for: groceries)   // "Card ending 4242"

let lastFour binds the associated value to a new constant for that case. The two .transfer cases show how far patterns go: let reference? matches only when the optional reference has a value and unwraps it, and nil matches when it doesn’t. The compiler still checks that, between them, every possibility is covered.

When you care about a single case, if case is shorter than a full switch:

Swift
if case .card(let lastFour) = groceries {
    print("Remind me to check card \(lastFour)")
}

Enums instead of boolean soup

Enums are the right tool whenever a value is “exactly one of these”. A classic example is loading data. The first draft usually looks like three separate properties: isLoading, rates, and errorMessage. Nothing stops all three from being set at once, so every screen has to guess which one wins. One enum removes the guessing:

Swift
enum LoadState {
    case idle
    case loading
    case loaded([String: Decimal])
    case failed(String)
}

Now the rates exist only in the .loaded case and the message only in .failed, and a switch in the view handles each state once. You’ll build exactly this when Pocket Budget starts fetching exchange rates. Reach for an enum whenever you catch yourself comparing strings or juggling booleans that should never be true together.

Your types can now describe Pocket Budget’s data precisely. Next you’ll give them shared abilities with protocols, such as “can be compared” or “has a stable id”, and add behaviour to existing types with extensions.

Key takeaways

  • An enum lists every valid value of a type, so a typo like “Fod” becomes a compile error instead of a silent bug.
  • [object Object]
  • Associated values attach different data to different cases, such as the last four digits for .card.
  • Switch over enums without default, so adding a case makes the compiler list every switch you must update.

Further reading

Check your understanding

3 questions

Question 1 of 3

enum Category: String { case food, transport }. What does Category(rawValue: "Food") return?

Question 2 of 3

You add case gifts to Category. Every switch over Category uses explicit cases and no default. What happens on the next build?

Question 3 of 3

Which declaration models “paid by card, and we know the last four digits”?

Practice

Fill in the blanks

Model categories and payment methods

Complete Pocket Budget’s two enums. Category must provide a list of all its cases so the picker can show them, and label(for:) must read the last four digits out of a card payment.

enum Category: String, { case food, transport, fun } print(Category..count) // 3 enum PaymentMethod { case cash case card(lastFour: String) } func label(for method: PaymentMethod) -> String { switch method { case .cash: return "Cash" case .card( lastFour): return "Card ending \(lastFour)" } }