Pattern matching
Swift’s pattern matching is one of its most distinctive features. The principal mechanism is the switch statement — exhaustive matching with substantial pattern grammar: value patterns (literals), wildcard (_), value-binding (let x), tuple patterns, enum case patterns (with associated values), type-casting patterns (as), range patterns (1...10), and expression patterns (using ~=). The where clause admits guards. Patterns also appear in for case, if case, guard case, while case, and destructuring assignment. The combination — exhaustive switch, the substantial pattern grammar, patterns outside switch, the where guards, the type-cast patterns — is the substance of Swift’s pattern-matching mechanism.
switch
The principal form:
switch value {
case pattern1:
body
case pattern2:
body
default:
body
}
Examples:
let n = 5
switch n {
case 0:
print("zero")
case 1, 2, 3: // multiple values
print("small")
case 4...10: // range
print("medium")
case let x where x.isMultiple(of: 5): // bound + guard
print("multiple of 5: \(x)")
default:
print("large")
}
The switch is exhaustive — every possible value of the discriminant must be covered, or the compiler refuses to compile:
let active: Bool = true
switch active {
case true: print("active")
case false: print("inactive")
// exhaustive — no default needed
}
For non-exhaustive coverage, default is required.
No fallthrough by default
Each case body runs once and exits the switch — no fallthrough:
switch n {
case 1:
print("one")
case 2:
print("two")
}
// Prints only "one" for n=1
For explicit fallthrough, the fallthrough keyword:
switch n {
case 1:
print("one")
fallthrough // explicit
case 2:
print("two") // also runs
}
The form is rare in idiomatic Swift; case 1, 2, 3 admits the conventional “match any of these” without fallthrough.
Value patterns
Match against literals or constants:
let n = 5
switch n {
case 0: print("zero")
case 1: print("one")
case 2: print("two")
default: print("other")
}
let s = "hello"
switch s {
case "hello": print("greeting")
case "goodbye": print("farewell")
default: print("other")
}
Wildcard _
The _ matches anything without binding:
let coord = (1, 2)
switch coord {
case (0, _): print("on y-axis")
case (_, 0): print("on x-axis")
case (_, _): print("anywhere")
}
Value-binding patterns
The let (or var) admits binding the matched value:
let coord = (1, 2)
switch coord {
case (0, let y): print("y-axis at \(y)")
case (let x, 0): print("x-axis at \(x)")
case let (x, y): print("(\(x), \(y))") // bind both
}
The pattern admits substantial decomposition with binding.
Tuple patterns
Tuples admit substantial pattern matching:
let point = (x: 1, y: 2)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case (let x, let y) where x == y:
print("diagonal")
case let (x, y):
print("(\(x), \(y))")
}
The form admits substantial multi-axis dispatch.
Enum case patterns
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case triangle(base: Double, height: Double)
}
func area(_ s: Shape) -> Double {
switch s {
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 b * h / 2
}
}
The .circle(let r) admits binding the associated value.
For let factored out:
switch s {
case let .circle(r):
return Double.pi * r * r
case let .rectangle(w, h):
return w * h
case let .triangle(b, h):
return b * h / 2
}
The let may appear inside the case (case .circle(let r)) or outside (case let .circle(r)); both forms are admitted.
For matching specific values:
switch s {
case .circle(0):
print("zero-radius circle")
case .circle(let r) where r > 100:
print("large circle")
case .circle(let r):
print("circle radius \(r)")
default:
break
}
Range patterns
let n = 42
switch n {
case 0:
print("zero")
case 1...10:
print("small")
case 11..<100: // half-open
print("medium")
case 100...: // one-sided
print("large")
default:
print("negative")
}
// Character ranges:
let c: Character = "g"
switch c {
case "a"..."m":
print("first half")
case "n"..."z":
print("second half")
default:
print("other")
}
Type-casting patterns
The is and as patterns admit type-based matching:
let value: Any = "hello"
switch value {
case is Int:
print("an Int")
case let s as String:
print("a String: \(s)")
case let arr as [Int]:
print("an [Int] of length \(arr.count)")
default:
print("unknown")
}
The pattern admits substantial discrimination on runtime type.
Optional patterns
Optionals admit ? and .some/.none:
let n: Int? = 42
switch n {
case .none:
print("nothing")
case .some(let value):
print("got \(value)")
}
// Or:
switch n {
case nil:
print("nothing")
case let value?:
print("got \(value)") // optional pattern
}
The let value? is the optional pattern — admits binding the wrapped value.
where guards
let n = 7
switch n {
case let x where x.isMultiple(of: 2):
print("even: \(x)")
case let x where x.isMultiple(of: 3):
print("multiple of 3: \(x)")
case let x where x.isMultiple(of: 5):
print("multiple of 5: \(x)")
default:
print("other")
}
The where admits substantial guards on bound values.
Multiple patterns per case
switch day {
case .saturday, .sunday:
print("weekend")
case .monday, .tuesday, .wednesday, .thursday, .friday:
print("weekday")
}
let coord = (1, 0)
switch coord {
case (0, 0), (1, 0), (0, 1):
print("special point")
default:
print("ordinary")
}
if case, guard case, while case, for case
Patterns appear outside switch:
if case
let value: Int? = 42
if case .some(let n) = value {
print(n)
}
if case let .some(n) = value, n > 0 {
print("positive: \(n)")
}
guard case
func process(value: Result<Int, Error>) {
guard case .success(let n) = value else {
return
}
print("got \(n)")
}
while case
var iter = arr.makeIterator()
while case let .some(item) = iter.next() {
process(item)
}
for case
let mixed: [Any] = [1, "hello", 2.0, "world", 3]
for case let s as String in mixed {
print("string: \(s)") // "hello", "world"
}
for case let n as Int in mixed where n > 1 {
print("int > 1: \(n)")
}
The form admits substantial filtered iteration with type discrimination.
Destructuring assignment
let pair = (1, "hello")
let (n, s) = pair // destructure
let coord = (x: 1, y: 2)
let (x, y) = coord
// In function parameters:
func process(_ point: (Int, Int)) {
let (x, y) = point
/* use x, y */
}
Common patterns
Discriminated union dispatch
enum Action {
case increment(by: Int)
case decrement(by: Int)
case reset
case set(value: Int)
}
func reduce(state: Int, action: Action) -> Int {
switch action {
case .increment(let n): return state + n
case .decrement(let n): return state - n
case .reset: return 0
case .set(let value): return value
}
}
State machine
enum State {
case idle
case running(startedAt: Date)
case completed(result: String)
case failed(error: Error)
}
func describe(_ state: State) -> String {
switch state {
case .idle:
return "Ready"
case .running(let date):
return "Running since \(date)"
case .completed(let result):
return "Done: \(result)"
case .failed(let error):
return "Error: \(error.localizedDescription)"
}
}
Result handling
let result: Result<Data, Error> = ...
switch result {
case .success(let data):
process(data)
case .failure(let error):
log(error)
}
Type-based dispatch
func describe(_ value: Any) -> String {
switch value {
case let n as Int: return "int \(n)"
case let d as Double: return "double \(d)"
case let s as String: return "string \(s)"
case let arr as [Any]: return "array \(arr.count) items"
case let dict as [String: Any]: return "dict \(dict.count) keys"
case is NSNull: return "null"
case nil: return "nil"
default: return "unknown: \(value)"
}
}
Range-based dispatch
func category(age: Int) -> String {
switch age {
case ..<13: return "child"
case 13..<20: return "teen"
case 20..<65: return "adult"
case 65...: return "senior"
default: return "invalid"
}
}
Tuple dispatch
func quadrant(_ x: Int, _ y: Int) -> Int {
switch (x, y) {
case (0, 0): return 0
case (let x, let y) where x > 0 && y > 0: return 1
case (let x, let y) where x < 0 && y > 0: return 2
case (let x, let y) where x < 0 && y < 0: return 3
case (let x, let y) where x > 0 && y < 0: return 4
default: return 0
}
}
Optional binding via if case
let value: Int? = 42
if case let n? = value, n > 0 {
print("positive: \(n)")
}
Filter with type-cast in for
for case let url as URL in mixedArray {
fetch(url)
}
Validation with guard case
func processResult(_ result: Result<User, Error>) async throws -> User {
guard case .success(let user) = result else {
throw AuthError.fetchFailed
}
return user
}
Switch as expression (Swift 5.9+)
let category = switch age {
case ..<13: "child"
case 13..<20: "teen"
case 20..<65: "adult"
default: "senior"
}
The expression-form admits substantial conciseness — particularly for value-returning conditional logic.
Enum with multiple associated values
enum Event {
case tap(point: CGPoint)
case key(code: UInt32, modifiers: UInt32)
case scroll(delta: Double)
}
func handle(_ event: Event) {
switch event {
case .tap(let point):
handleTap(point)
case .key(let code, let modifiers):
handleKey(code: code, modifiers: modifiers)
case .scroll(let delta):
scroll(by: delta)
}
}
Recursive enum
indirect enum Expr {
case literal(Int)
case add(Expr, Expr)
case mul(Expr, Expr)
}
func eval(_ e: Expr) -> Int {
switch e {
case .literal(let n): return n
case .add(let a, let b): return eval(a) + eval(b)
case .mul(let a, let b): return eval(a) * eval(b)
}
}
let expr = Expr.add(.literal(2), .mul(.literal(3), .literal(4)))
print(eval(expr)) // 14
The indirect admits the enum to recursively reference itself.
Match with side effect via where
switch userInput {
case let s where s.matches(/^\d+$/):
parseNumber(s)
case let s where s.matches(/^[a-z]+$/):
parseIdentifier(s)
default:
error()
}
Pattern matching with ~=
The ~= is the pattern match operator — admits explicit matching outside switch:
let n = 5
if 1...10 ~= n {
print("in range")
}
// Custom ~=:
struct Even {}
func ~= (pattern: Even, value: Int) -> Bool {
value.isMultiple(of: 2)
}
if Even() ~= 4 {
print("4 is even")
}
// And in switch:
switch n {
case Even():
print("even")
default:
print("odd")
}
The mechanism admits substantial custom pattern matching.
A note on the conventional discipline
The contemporary Swift pattern-matching advice:
- Use
switchfor value-driven dispatch. - Trust exhaustiveness — the compiler verifies all cases are covered.
- Use enum case patterns with associated values for substantial discrimination.
- Use
wherefor guards. - Use
if case/guard casefor inline pattern matching. - Use
for casefor filtered iteration. - Use type-casting patterns (
as,is) for runtime-type discrimination. - Use range patterns for substantial value ranges.
- Use
switchas expression (5.9+) for substantial conciseness. - Avoid
fallthrough— multiple values per case is conventionally clearer. - Use
_wildcards freely.
The combination — exhaustive switch, the substantial pattern grammar, patterns in if case/guard case/for case/while case, where guards, the expression-form, the ~= for custom matching — is the substance of Swift’s pattern matching. The discipline produces substantial, type-safe dispatch with substantial expressiveness for substantial discriminated-union code.