Enums and associated values
Swift's enums carry data, which makes them the tool for stopping illegal states from being representable at all.
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
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"
}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.
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.
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.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.
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.
Why can an enum not have stored properties?
Model a network result
Define a FetchResult enum with three cases — success carrying a [String], failure carrying a message, and cancelled — then print a description of each.
Parse raw values safely
Convert a list of strings into Theme values, skipping anything that does not match a case, and print what survived.
Replace the booleans
Rewrite this state as an enum so that impossible combinations cannot be expressed, then render each state.
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.
You can now:
- Define enums with raw values, associated values, methods and computed properties
- Use
CaseIterableand failable raw-value initializers - Recognise a boolean soup and replace it with an enum
- Explain when
indirectis needed
Next up: protocols — describing what a type can do rather than what it is, and the foundation of everything SwiftUI does.