Pattern Matching

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

A pattern is what appears on the left of a case (or after if/for/while case, or in a catch): a shape that a value either matches or doesn’t, optionally extracting pieces of it into new constants or variables along the way. Swift defines exactly eight pattern kinds, and the same grammar is shared by every construct that matches something against a case.

The Eight Pattern Kinds

let point = (2, 0)

switch point {
case _:                            // wildcard pattern -- matches anything, binds nothing
    break
}

switch point {
case let anyPoint:                 // identifier pattern -- matches anything, binds it to a new name
    print(anyPoint)
}

switch point {
case (let x, 0):                   // value-binding pattern -- `let`/`var` inside a larger pattern
    print("on the x-axis at \(x)")
default:
    break
}

switch point {
case (0, 0):                       // tuple pattern -- matches each element position
    print("origin")
default:
    break
}

enum Direction { case north, south, east, west }
let heading = Direction.north
switch heading {
case .north:                       // enumeration case pattern
    print("heading up")
default:
    break
}

let maybeNumber: Int? = 7
switch maybeNumber {
case .some(let number):             // optional pattern (`case let x?` is sugar for this)
    print(number)
case .none:
    print("nothing")
}

let value: Any = 42
switch value {
case is String:                     // type-casting pattern (`is`)
    print("a string")
case let intValue as Int:            // type-casting pattern (`as`), also binds
    print("an Int: \(intValue)")
default:
    break
}

switch 5 {
case 0..<10:                        // expression pattern -- matches via the `~=` operator (here, Range's)
    print("single digit")
default:
    break
}
Pattern kind Where it typically appears

Wildcard (_)

anywhere a value is matched but not needed

Identifier

anywhere — always matches, just binds a name

Value-binding (let/var)

inside a tuple, enum-case, or optional pattern

Tuple

switch, if case, for case, while case

Enumeration case (.foo)

switch, if case, for case, while case

Optional (x?)

switch, if case/if let, for case, while case/while let

Type-casting (is/as)

switch, if case, catch

Expression (via ~=)

switch, if case

Where Each Pattern May Appear

for case let .some(value) in [1, nil, 3, nil, 5] {     // for case: skips elements that don't match
    print(value)                                          // 1 3 5 -- nil elements are silently skipped
}

var stack = [1, 2, 0, 4]
while case let top? = stack.popLast(), top != 0 {        // while case: loops only while the pattern matches
    print(top)
}

do {
    throw CustomError.timeout(seconds: 30)
} catch CustomError.timeout(let seconds) {                 // catch matches a pattern too, like switch's cases
    print("timed out after \(seconds)s")
} catch {
    print("some other error")
}

enum CustomError: Error { case timeout(seconds: Int) }

switch, if case, for case, while case and catch all match a value against one or more patterns using the identical grammar above; only switch and catch require the full set of cases to be exhaustive.

Nested Destructuring

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
}

let shapes: [(String, Shape)] = [("a", .circle(radius: 2)), ("b", .rectangle(width: 3, height: 4))]

for (label, shape) in shapes {
    switch (label, shape) {                                          // patterns nest inside patterns freely
    case let (name, .circle(radius)) where radius > 1:
        print("\(name): a circle bigger than radius 1 (r = \(radius))")
    case let (name, .rectangle(width, height)):
        print("\(name): a \(width)x\(height) rectangle")
    default:
        print("\(label): something else")
    }
}

let points = [(0, 0), (1, 1), (2, 4)]
for case (let x, let y) in points where y == x * x {                  // tuple pattern + where, nested in for case
    print("(\(x), \(y)) lies on y = x^2")
}

Any pattern kind can nest inside any other — a tuple pattern’s elements can themselves be enum-case patterns, which can themselves bind values, all matched and destructured in one case.

Custom ~= Overloads

struct Interval {
    let lowerBound: Int
    let upperBound: Int
}

func ~= (interval: Interval, value: Int) -> Bool {                    // teaches switch how to match `value`
    interval.lowerBound...interval.upperBound ~= value
}

let score = 82
switch score {
case Interval(lowerBound: 90, upperBound: 100): print("A")
case Interval(lowerBound: 80, upperBound: 89): print("B")            // matches via the custom ~= above
default: print("C or below")
}

An expression pattern matches by calling ~=(pattern, value) and checking whether it returns true — the standard library already overloads it for Range/ClosedRange (matching membership) and Equatable (matching ==), and defining your own overload, as above, lets switch accept a custom type as a case pattern.

Matching Against Ranges and Regular Expressions

let bmi = 24.5
switch bmi {
case ..<18.5: print("underweight")
case 18.5..<25: print("healthy weight")                              // range patterns via the built-in `~=`
case 25..<30: print("overweight")
default: print("obese")
}

let line = "error: 42 issues found"
if case let match = try! Regex(#"\d+"#).firstMatch(in: line), let match {
    print("first number: \(line[match.range])")                       // "42"
}

switch "user@example.com" {
case /\w+@\w+\.\w+/:                                                    // a regex literal used directly as a pattern
    print("looks like an email address")
default:
    print("not an email address")
}

A range on the left of case matches through the same ~= mechanism as any expression pattern; a Regex value or regex literal on the left of case matches the same way, via the standard library’s ~=(Regex<Output>, String) overload — see Regular Expressions for Regex itself, its literal syntax, and capture groups.

How switch Evaluates Its Cases

flowchart TD A["switch value {"] --> B["Case 1 pattern"] B -->|"matches, and any where clause is true"| M1["Run Case 1's body, then exit the switch"] B -->|"no match, or where is false"| C["Case 2 pattern"] C -->|"matches"| M2["Run Case 2's body, then exit the switch"] C -->|"no match"| D["... remaining cases, in source order"] D -->|"none match"| E{"Is a default: case\npresent?"} E -->|"yes"| M3["Run default's body"] E -->|"no"| F["Compile error: switch must be exhaustive"] style M1 fill:#eefaf2,stroke:#2f7d51 style M2 fill:#eefaf2,stroke:#2f7d51 style M3 fill:#eefaf2,stroke:#2f7d51 style F fill:#fdf3f3,stroke:#b5523d

Cases are tried strictly top to bottom; the first one whose pattern matches — and whose where clause, if any, evaluates true — runs, and (without an explicit fallthrough) the switch exits immediately afterward, never falling through to a later case the way C’s switch does by default.

See Also

  • Control Flow — switch/if case/guard case in their full statement/expression context.

  • Optionals — the optional pattern (x?) in depth, and map/flatMap as its non-pattern-matching alternative.

  • Enumerations — enumeration-case patterns and associated values, the type these patterns exist to destructure.

  • Regular Expressions — Regex, regex literals, and capture groups used above.