Using ‘@unknown default’ within switch statements
Basics article available: EnumsWhen 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!