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
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:
@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:
Common mistakes¶
- Adding a
defaultto your own enum'sswitch— 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
indirectfor recursive enums ("recursive enum is not marked 'indirect'"). - Force-unwrapping
init(rawValue:)on external input — handlenil.
Exercises¶
- Model a traffic
Lightenum with a computedduration: Intper state and anextproperty. - Define
enum PaymentMethod { case cash, card(last4: String), transfer(iban: String) }and writedescribe(_:). - Build the
Exprexample further: add.divide, and makeevaluatereturnDouble?(division by zero →nil). - Write
func parse(_ s: String) -> Expr?for tiny inputs like"2+3"(single operator) — a warm-up for the capstone-style parsers. - Use
for caseto count how many.failedevents are in an array ofLoadState. - Create an enum
Token(number(Int),plus,minus,leftParen,rightParen) and write a tokenizer fromStringto[Token].
Recap¶
- Enums define closed sets of cases;
switchover them is exhaustive. - Raw values map to external representations; associated values carry per-case data.
if case,guard case, andfor casepattern-match without a fullswitch.indirectenables recursive structures;OptionalandResultare just enums.- Avoid
defaulton your own enums so the compiler can help you refactor.
Next: Protocols →