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:

  1. Does false have a clear, intuitive meaning? If you have to think about what "not X" means, consider an enum.

  2. Could a third state ever exist? Even if it doesn't today, enums make future extension painless.

  3. Is the boolean being negated a lot? If you're constantly writing !isWhatever, the logic might be clearer with named cases.

  4. Would a new developer understand the code? If false requires 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.