Swift Enums and Pattern Matching

Enums in Swift let you model a fixed set of related cases, and switch statements pair with them to guarantee every case is handled.

Defining a Basic Enum

An enum groups related values under one type, where each possible value is called a case. Unlike enums in many other languages, Swift enums are first-class types that can have methods, computed properties, and even protocol conformances. A switch statement over an enum must be exhaustive, covering every case (or providing a default), which means the compiler will flag you the moment you add a new case somewhere and forget to handle it elsewhere.

A basic enum with switch

enum Direction {
    case north, south, east, west
}

let heading = Direction.north

switch heading {
case .north:
    print("Heading north")
case .south:
    print("Heading south")
case .east, .west:
    print("Heading east or west")
}

Enums with Raw Values

Sometimes each case naturally maps to an underlying value, like an HTTP status code or an ordinal position. Declaring an enum as Int, String, or another RawRepresentable type lets every case carry a constant raw value, accessible through .rawValue. You can also go the other direction, constructing a case from its raw value using a failable initializer that returns an optional.

Raw value enums

enum Planet: Int {
    case mercury = 1, venus, earth, mars
}

let earthOrder = Planet.earth.rawValue   // 3

if let planet = Planet(rawValue: 4) {
    print("Found \(planet)")   // Found mars
}

Associated Values

Raw values are fixed and identical for every instance of a case, but associated values let each instance of a case carry its own unique data — a completely different capability. This is how Swift models things like a network response that is either a success carrying data, or a failure carrying an error code and message. Pattern matching in switch can bind those associated values to local constants, and a where clause can further filter which pattern matches.

Associated values with pattern matching

enum NetworkResponse {
    case success(data: String)
    case failure(code: Int, message: String)
    case loading
}

func handle(_ response: NetworkResponse) {
    switch response {
    case .success(let data):
        print("Received: \(data)")
    case .failure(let code, let message) where code >= 500:
        print("Server error \(code): \(message)")
    case .failure(let code, let message):
        print("Error \(code): \(message)")
    case .loading:
        print("Still loading...")
    }
}

handle(.failure(code: 503, message: "Service unavailable"))
  • where clauses narrow a case pattern with an extra boolean condition
  • let/var bindings inside a pattern extract associated or tuple values
  • Tuple patterns let a single switch match on several values at once
  • Range patterns (1...5, ..<10) match against a range of values
  • The underscore _ ignores a value you don't care about in a pattern
FeatureRaw Value EnumAssociated Value Enum
StorageOne fixed constant shared by the caseCustom data attached to each instance
ConformanceAutomatically Equatable/Hashable, can be CaseIterableNeeds manual conformance if extra data isn't Equatable
Typical useA fixed known set mapped to values, like HTTP status codesVariable payloads per case, like API responses or app states
Note: Because switch statements over enums must be exhaustive, adding a new case to an enum is a safe, traceable change: the compiler produces an error at every switch statement in your codebase that doesn't yet account for it, rather than letting the new case silently fall through unhandled.

Exercise: Swift Enums

Are Swift enums value types or reference types?