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.
- 1Enums with raw values
- 2Exhaustive switches are a feature
- 3Associated values: cases that carry data
- 4Getting the data back out
- 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
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:
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.
Getting the data back out
You read associated values with pattern matching in a switch:
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:
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:
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.