What are enums with associated values, and when do you use them?
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 values | Associated values | |
|---|---|---|
| Data | Same type for all cases | Different per case |
| Fixed? | Yes, set at compile time | No, set when you create the value |
| Example | case north = "N" | case card(last4: String) |
init(rawValue:) | Yes | No |
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)))) // 14Common mistakes
- Using a
default:case in aswitch. 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 thancase card(String).
Common follow-up questions
- Is
Optionalan enum with associated values? Yes.case some(Wrapped)andcase none. - Is
Resultone too? Yes.case success(Success)andcase failure(Failure). - Can enums have methods and computed properties? Yes. They can also conform to protocols and have
staticmembers. 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.