How do NS_ENUM, NS_OPTIONS and NS_CLOSED_ENUM import, and why does Swift demand @unknown default?
Predict first
currentMode() is an Objective-C function that returns (Mode)7, but the enum has three cases. What does this print?
// Objective-C: typedef NS_ENUM(NSInteger, Mode) { ModeOff, ModeOn, ModeAuto };
// Mode currentMode(void) { return (Mode)7; }
func describe(_ m: Mode) -> String {
switch m {
case .off: return "off"
case .on: return "on"
case .auto: return "auto"
@unknown default: return "unknown(\(m.rawValue))"
}
}
print(describe(.on), describe(currentMode()))
- on auto
- on unknown(7)
- a crash
Predict first
Does this compile in Swift 6 mode if Mode is an NS_ENUM?
// Objective-C: typedef NS_ENUM(NSInteger, Mode) { ModeOff, ModeOn, ModeAuto };
func a(_ m: Mode) -> String {
switch m { case .off: "off"; case .on: "on"; case .auto: "auto" }
}
- Yes
- No: the switch needs @unknown default
Predict first
And with NS_CLOSED_ENUM(NSInteger, Side) (cases SideLeft, SideRight)?
// Objective-C: typedef NS_CLOSED_ENUM(NSInteger, Side) { SideLeft, SideRight };
func b(_ s: Side) -> String {
switch s { case .left: "l"; case .right: "r" }
}
print(b(.left))
- It compiles
- It needs @unknown default
Mental model. An imported enum is a menu from a restaurant that may add dishes after you printed your order sheet. @unknown default is the line for “anything else”. A closed enum is a menu carved in stone.
NS_ENUM(NSInteger, Mode) imports as a Swift enum with the common prefix removed (ModeOn becomes .on) and a rawValue of the declared integer type. NS_OPTIONS imports as an OptionSet struct, so you write [.read, .write] and test with contains. A plain C enum without these macros imports as a struct of constants, which is why the macros matter. Both enums and option sets keep their C representation: an NS_ENUM can hold any integer, including a value that has no case.
That is why Swift treats an imported enum as non-frozen and insists on @unknown default in a switch: a newer framework (or buggy code) may deliver a value your code has never heard of. It is a warning in Swift 5 mode and an error in Swift 6 mode. If the declaration will genuinely never grow, NS_CLOSED_ENUM tells Swift it is frozen and the exhaustive switch needs no default. Use it only for sets like left and right; a wrong promise would make Swift take impossible branches. For an error domain, NS_ERROR_ENUM gives the codes a Swift enum that conforms to Error.
The value 7 is stored in an integer and no case matches it, so only the @unknown default branch can handle it. There is no crash and no trap.
The enum is non-frozen, so the compiler reports that it may have additional unknown values. In Swift 5 mode it is only a warning.
A closed enum is frozen, so the two cases are exhaustive and Swift needs no default.
Common traps
- Casting a raw integer with
Mode(rawValue: 7)and trusting a non-optional. For a C enum the initializer is non-failable on some imports; the value simply exists. Handle it in the switch. - Marking a growing enum
NS_CLOSED_ENUM. Swift will treat a new case as impossible.
They might follow up with
- Why does an
NS_OPTIONSvalue keep unknown bits? It is still an unsigned integer under the struct. - How do Swift enums reach Objective-C? Only
@objc enum Name: Intwith no payloads does; it appears asNS_ENUM.
Related questions
- 4.4.2 Which Swift features cannot be seen from Objective-C?
- 4.5.1 How do nullability annotations change what Swift sees, and what happens when they lie?
How do Objective-C initializers and factory methods import, and what… →