Articles, podcasts and news about Swift development, by John Sundell.

Using ‘@unknown default’ within switch statements

Published on 06 Aug 2021
Basics article available: Enums

When switching on an enum, we are required to either explicitly handle all of its cases, or provide a default case that’ll act as a fallback for cases that weren’t matched by any previous case statement. For example like this:

struct Article {
    enum State {
        case draft
        case published
        case removed
    }

    var state: State
    ...
}

// Explicitly handling all possible cases:

func articleIconColor(forState state: Article.State) -> Color {
    switch state {
    case .draft:
        return .yellow
    case .published:
        return .green
    case .removed:
        return .red
    }
}

// Using a default case:

extension Article {
    var isDraft: Bool {
        switch state {
        case .draft:
            return true
        default:
            return false
        }
    }
}

Fun fact: Instead of a default case, we can also use case _ to match all possible patterns within a switch statement. The two are functionally identical, since _ acts as a “wild card” within Swift’s pattern matching system.

When possible, I always recommend writing exhaustive switch statements that handle each possible case explicitly, even if that involves a bit more code. The reason is that doing so will give us a compiler error if we ever add a new case in the future, which in turn “forces us” to make a proper decision on how to handle that new case across our code base. Default cases might be convenient, but they can quickly become a common source of bugs when an old piece of code ends up dealing with an enum case that it wasn’t designed to handle, simply because it was called from within a default statement.

So here’s how I’d personally implement the above isDraft property:

extension Article {
    var isDraft: Bool {
        switch state {
        case .draft:
            return true
        case .published, .removed:
    return false
}
    }
}

However, when working with certain system-provided enums, we sometimes need to use a default case, since those enums might be updated with new cases at any point. For example, if we tried to exhaustively switch on something like UIKit’s UIUserInterfaceStyle enum, then the compiler would give us a warning:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        // ⚠️ Warning: Switch covers known cases, but
        // 'UIUserInterfaceStyle' may have additional unknown
        // values, possibly added in future versions.
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            return false
        }
    }
}

One way to address the above warning would of course be to use a standard default statement (as that would catch any additional cases that might be added in the future), but then we’re back in that situation where our code might end up doing the wrong thing when handling those new cases.

Thankfully, Swift has a built-in solution to this problem, and that’s the @unknown attribute, which can be attached to either a default or case _ statement in order to handle any cases that are unknown at the time when we’re writing our code, while still producing warnings if we forget to handle an existing case. Here’s how we could apply that attribute to the above isUsingDarkMode implementation:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            return false
        @unknown default:
    return false
}
    }
}

Note how we need to write our @unknown default case as a separate statement, rather than combining it with another case that results in the same outcome. One way to work around that limitation would be to use the fallthrough keyword, which will cause Swift to automatically move to the next case within our switch statement — like this:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            fallthrough
        @unknown default:
            return false
        }
    }
}

The above is not really a pattern that I personally use, since I prefer to clearly separate my @unknown default fallback code from the code that deals with known cases, but I still thought that it was worth mentioning that technique.

So how come @unknown default statements are only required (or at least recommended) when switching on certain specific enums? This is where the concept of frozen enums come in. When an enum is marked as frozen, that tells the compiler that it’ll never (or at least should never) gain any new cases, which means that it’s safe for us to exhaustively switch on its cases without needing to handle any unknown ones.

For example, if we take a look at what Foundation’s ComparisonResult enum’s Swift interface looks like, then we can see that it’s marked with the @frozen attribute:

@frozen public enum ComparisonResult: Int {
    case orderedAscending = -1
    case orderedSame = 0
    case orderedDescending = 1
}

That means that we’re free to switch on ComparisonResult values (and others like it) without being warned that we should add an @unknown default case.

So does that mean that we should also add that same @frozen attribute to our own enums as well? Not really, since the compiler will automatically treat all user-defined Swift enums as frozen by default. However, if we’re working with enums that we’ve defined in Objective-C, then we’ll have to explicitly mark them as “closed for extensibility” if we want them to become frozen when imported into Swift:

typedef NS_ENUM(NSInteger, SXSArticleState) {
    SXSArticleStateDraft,
    SXSArticleStatePublished,
    SXSArticleStateRemoved
} __attribute__((enum_extensibility(closed))) NS_SWIFT_NAME(ArticleState);

Alternatively, we could replace NS_ENUM with NS_CLOSED_ENUM to have the above extensibility attribute be automatically applied.

With the above in place, our SXSArticleState type will now be imported into Swift as a frozen enum called ArticleState, and it’ll work exactly like our earlier, Swift-native Article.State enum did.

I hope that this article has given you a few insights into how @unknown default statements work and why they’re sometimes needed. If you have any questions, comments, or feedback, then feel free to reach out.

Thanks for reading!