Sometimes a Bool Isn't the Answer: Two-Value Enums in Swift
Booleans are great. They're simple, everyone understands them, and Swift gives us all the syntactic sugar we could want for working with them. But sometimes, reaching for a Bool creates more confusion than it solves.
Let me show you what I mean.
The Problem with isMaximised
Imagine you're building a view that can be maximised or minimised. Your first instinct might be:
struct ContentView: View {
@State private var isMaximised: Bool = false
var body: some View {
// ...
}
}
Seems fine, right? But here's the thing — when you're reading the code, false doesn't scream "minimised". It screams "not maximised", which isn't quite the same thing conceptually.
And what happens when you need to toggle it?
isMaximised.toggle()
Clear enough. But what about when you're setting it explicitly?
isMaximised = false // Is this minimised? Or just "not maximised"?
Now imagine this view state being passed around, checked in multiple places, maybe even persisted. Every time someone reads if !isMaximised, they have to mentally translate "not maximised" into "minimised".
Enter the Two-Value Enum
enum ViewState {
case maximised
case minimised
}
struct ContentView: View {
@State private var viewState: ViewState = .minimised
var body: some View {
// ...
}
}
Now when you read the code, there's no translation required:
viewState = .minimised // Crystal clear
viewState = .maximised // Also crystal clear
switch viewState {
case .maximised:
showFullScreenLayout()
case .minimised:
showCompactLayout()
}
The switch is exhaustive by default, so if you ever add a third state (say, .collapsed), the compiler will helpfully point out every place you need to handle it.
A More Practical Example: Loading States
Here's one that comes up constantly:
// Bool approach
@State private var isLoading: Bool = false
// What does false mean here?
// - Never started loading?
// - Finished loading successfully?
// - Failed to load?
Versus:
enum LoadState {
case idle
case loading
}
@State private var loadState: LoadState = .idle
Now .idle and .loading are distinct, named concepts. And when requirements change (they always do), you can extend it:
enum LoadState {
case idle
case loading
case loaded
case failed(Error)
}
The compiler catches every place that needs updating. Try doing that with a Bool.
When Bools Are Still the Right Call
I'm not saying you should replace every boolean in your codebase. Bools are perfect when:
The property genuinely represents a true/false condition:
let isEmpty: Bool
let isEnabled: Bool
let hasUnsavedChanges: Bool
These read naturally as assertions. "Is it empty? Yes or no." There's no hidden third state lurking.
The negative case is just "not the positive case":
let isVisible: Bool // Not visible = hidden. Same concept, inverted.
You're implementing a protocol or matching an expected API:
var isSelected: Bool // UIKit expects this, don't fight it
The Swift Guidelines Perspective
I checked the Swift API Design Guidelines to see if there was any official stance on two-value enums. There isn't — and that's kind of the point.
The guidelines emphasise that "clarity at the point of use" is the most important goal. They don't care how many cases your enum has; they care whether your code is readable.
They do say that boolean properties should "read as assertions about the receiver" — like isEmpty or isEnabled. If your boolean doesn't read naturally as a yes/no question, that's a hint you might want something more expressive.
The Taste Test
When deciding between a Bool and a two-value enum, ask yourself:
Does
falsehave a clear, intuitive meaning? If you have to think about what "not X" means, consider an enum.Could a third state ever exist? Even if it doesn't today, enums make future extension painless.
Is the boolean being negated a lot? If you're constantly writing
!isWhatever, the logic might be clearer with named cases.Would a new developer understand the code? If
falserequires context to interpret, named cases help.
A Quick Implementation Pattern
If you're sold on the enum approach but miss the convenience of toggle(), you can add it yourself:
enum ViewState {
case maximised
case minimised
mutating func toggle() {
self = (self == .maximised) ? .minimised : .maximised
}
var isMaximised: Bool {
self == .maximised
}
}
Now you get the clarity of named states and the convenience of a toggle. Best of both worlds.
Wrapping Up
Two-value enums aren't a replacement for booleans — they're an alternative for when booleans get confusing. The goal isn't purity; it's clarity.
Next time you reach for a Bool and find yourself pausing to think about what false represents, consider whether an enum might make your intent clearer. Your future self (and your teammates) will thank you.