iosNudge

What are enums with associated values, and when do you use them?

EasySwift 2 min read

TL;DR

An enum case can carry extra data. This data is called an associated value. Each case can carry different types. You read the data with switch or if case. Use it to model states, results and events, so that invalid states cannot exist.

Basic example

enum PaymentMethod {
    case cash
    case card(last4: String)
    case applePay(token: String, deviceName: String)
}

let method = PaymentMethod.card(last4: "4242")

cash has no data. card has one value. applePay has two.

Reading the values

With switch

switch method {
case .cash:
    print("Pay with cash")
case .card(let last4):
    print("Card ending \(last4)")
case let .applePay(_, deviceName):
    print("Apple Pay on \(deviceName)")
}

The switch must be exhaustive. If you add a new case later, the compiler shows every place to update. This is a big safety win.

With if case or guard case

if case .card(let last4) = method {
    print(last4)
}

Use this when you only care about one case.

Model screen state

This is the most common interview use.

enum LoadState<Value> {
    case idle
    case loading
    case loaded(Value)
    case failed(any Error)
}

struct ProfileView: View {
    @State private var state: LoadState<User> = .idle

    var body: some View {
        switch state {
        case .idle, .loading: ProgressView()
        case .loaded(let user): Text(user.name)
        case .failed(let error): Text(error.localizedDescription)
        }
    }
}

Compare with separate properties:

var isLoading = false
var user: User?
var error: Error?

Here isLoading == true and error != nil can both be true at the same time. That state makes no sense. The enum makes it impossible.

Associated values vs raw values

Raw valuesAssociated values
DataSame type for all casesDifferent per case
Fixed?Yes, set at compile timeNo, set when you create the value
Examplecase north = "N"case card(last4: String)
init(rawValue:)YesNo

An enum cannot have both raw values and associated values.

Protocol conformances

Swift synthesizes Equatable and Hashable if all associated values conform.

enum Route: Hashable {
    case home
    case profile(id: Int)
    case settings
}

let path: [Route] = [.home, .profile(id: 7)]

This works well with NavigationStack(path:) in SwiftUI.

Codable is also synthesized (since Swift 5.5). CaseIterable is not synthesized when a case has associated values, because Swift cannot list all possible values.

Recursive enums with indirect

An enum that contains itself needs indirect. Swift then stores that case on the heap.

indirect enum Expr {
    case number(Int)
    case add(Expr, Expr)
    case multiply(Expr, Expr)
}

func eval(_ e: Expr) -> Int {
    switch e {
    case .number(let n): n
    case .add(let a, let b): eval(a) + eval(b)
    case .multiply(let a, let b): eval(a) * eval(b)
    }
}

eval(.add(.number(2), .multiply(.number(3), .number(4))))   // 14

Common mistakes

  • Using a default: case in a switch. You lose the compiler warning when a new case is added.
  • Using many optional properties instead of one enum.
  • Forgetting labels. case card(last4: String) reads better than case card(String).

Common follow-up questions

  • Is Optional an enum with associated values? Yes. case some(Wrapped) and case none.
  • Is Result one too? Yes. case success(Success) and case failure(Failure).
  • Can enums have methods and computed properties? Yes. They can also conform to protocols and have static members. They cannot have stored properties.
  • What is @unknown default? Used when switching over an enum from another module (like Apple SDKs) that might add cases later. You get a warning if a new case appears.
  • How much memory does an enum use? The size of the largest payload plus a small tag. Swift often packs the tag into spare bits.