Skip to content

08 · Enums

Swift enums are far more capable than a simple list of named constants — they can carry associated data per case, have methods, conform to protocols, and are a first-class tool for modeling state.

Basic enums

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

let heading = Direction.north

// Type can be inferred at the use site once the variable's type is known:
var currentDirection: Direction = .south

switch currentDirection {
case .north:
    print("Heading up")
case .south:
    print("Heading down")
case .east, .west:
    print("Heading sideways")
}
// Heading down

switch over an enum must be exhaustive — add a new case later and the compiler flags every switch that forgot to handle it, a huge help when refactoring.

Raw values

enum Direction: String {
    case north = "N"
    case south = "S"
    case east = "E"
    case west = "W"
}

print(Direction.north.rawValue)   // N

let fromRaw = Direction(rawValue: "S")
print(fromRaw!)                    // south
print(Direction(rawValue: "Q"))     // nil -- invalid raw value
enum Priority: Int {
    case low = 1
    case medium
    case high
}
print(Priority.medium.rawValue)   // 2 -- auto-incremented from "low"

Associated values

Unlike raw values (one fixed value per case, shared across all instances of that case), associated values let each instance of a case carry its own data.

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

func handle(_ result: NetworkResult) {
    switch result {
    case .success(let data):
        print("Got data: \(data)")
    case .failure(let code, let message):
        print("Error \(code): \(message)")
    case .loading:
        print("Still loading...")
    }
}

handle(.success(data: "user profile"))
// Got data: user profile
handle(.failure(code: 404, message: "Not Found"))
// Error 404: Not Found
handle(.loading)
// Still loading...

This is the same shape as Optional itself, which is really just an enum under the hood:

// Swift's standard library, conceptually:
// enum Optional<Wrapped> {
//     case none
//     case some(Wrapped)
// }

Methods and computed properties on enums

enum Direction: String {
    case north = "N", south = "S", east = "E", west = "W"

    func opposite() -> Direction {
        switch self {
        case .north: return .south
        case .south: return .north
        case .east: return .west
        case .west: return .east
        }
    }
}

print(Direction.north.opposite())   // south

CaseIterable

enum Suit: CaseIterable {
    case clubs, diamonds, hearts, spades
}

for suit in Suit.allCases {
    print(suit)
}
// clubs
// diamonds
// hearts
// spades

print(Suit.allCases.count)   // 4

Enums with methods that mutate self

enum TrafficLight {
    case red, yellow, green

    mutating func next() {
        switch self {
        case .red: self = .green
        case .green: self = .yellow
        case .yellow: self = .red
        }
    }
}

var light = TrafficLight.red
light.next()
print(light)   // green
light.next()
print(light)   // yellow

Cheat sheet

Feature Example Notes
Basic case case north No associated data
Raw value case low = 1 One fixed value per case, shared by all instances
Associated value case success(data: String) Different data per instance of the case
rawValue init Direction(rawValue: "N") Returns an Optional — invalid input yields nil
CaseIterable Suit.allCases Iterate every case automatically
mutating func Changes self to a different case Requires the enum variable to be var

How It Actually Works

An enum without associated values is just a small integer under the hood. With ≤256 cases and no payload, Swift represents a value as a single byte holding a discriminator tag — Direction.north compiles to essentially the constant 0. The compiler picks the smallest integer width that fits the case count, so simple enums are as cheap as a raw Int8, and switch over one compiles to a jump table, not a chain of comparisons — that's part of why exhaustiveness matters: the compiler is building a table sized to the case count, and a new case genuinely requires a new table entry, which is what it flags as a compile error.

Enums with associated values are tagged unions — the same discriminator tag, plus enough storage for the largest case's payload. NetworkResult's layout reserves space for a tag byte and the largest of (String), (Int, String), or nothing — every instance is sized for the worst case, with unused bytes for smaller cases. This is exactly what a C union with a manual tag field does, except the compiler generates and enforces the tag/switch correspondence for you, which is why you can't read .success's data without going through a switch or if case — there's no type-safe way to know which payload variant is live except by checking the tag.

Indirect cases and recursion: an enum case whose payload contains the enum itself (e.g. a case node(value: Int, next: LinkedList)) can't have a fixed size — the compiler would need infinite storage to lay it out directly. Marking the case (or the whole enum) indirect makes Swift box that case's payload on the heap and store just a pointer inline, exactly the same mechanism a class provides implicitly, opted into per-case.

Why Optional really is "just an enum": Optional<Wrapped>.none and .some(Wrapped) follow the identical tagged-union layout described above — which is also why if let and switch work uniformly across Optional and your own enums; they're the same runtime shape, not special-cased syntax.

🔀 See this in another language

Exercise

Model a vending machine with enum VendingItem: String, CaseIterable { case soda, chips, candy } where each case has a price via a computed property (a switch over self inside the property). Then write an enum PurchaseResult with associated values for .success(item: VendingItem, change: Double) and .failure(reason: String), and a function purchase(item:, inserted:) -> PurchaseResult that returns .failure if inserted is less than the item's price, otherwise .success with the correct change.