NS_ENUM and NS_OPTIONS
Learn to model mutually exclusive states with NS_ENUM and combinable flags with NS_OPTIONS, including the bitwise operations -- OR to combine, AND to check, AND-NOT to clear -- that make NS_OPTIONS values work, and understand how each macro imports differently into Swift so that Objective-C APIs read naturally on both sides of the bridge.
Plain C enum has always been usable in Objective-C, but it says nothing about how large the underlying type is or whether its values are meant to be combined together, which leaves both the compiler and anyone reading the code guessing. NS_ENUM and NS_OPTIONS are two thin macros, both ultimately built on C's enum, that close that gap: NS_ENUM declares a fixed-size enumeration whose values are meant to be mutually exclusive -- exactly one state at a time -- while NS_OPTIONS declares a fixed-size bitmask enumeration whose values are meant to be combined together with bitwise OR. Picking the right one is a small decision with a large payoff: it documents the API's intent directly in its type, it determines the exact underlying storage size, and, as this lesson's final sections cover, it determines whether Swift code sees the type as an enum or as an OptionSet. This lesson builds on the Objective-C Literals and Syntax Sugar lesson's coverage of Objective-C's lighter-weight declaration forms and sets up the upcoming Swift API Customization and Swift Interoperability lessons, both of which depend on getting NS_ENUM and NS_OPTIONS right at the Objective-C side first.
Why NS_ENUM and NS_OPTIONS Exist
A bare C enum declaration like enum { Idle, Running, Completed, Failed }; compiles, but it leaves two important things unspecified: the compiler is free to choose any integer type large enough to hold the values, which can vary by platform and compiler version, and nothing in the declaration tells a reader -- or a tool -- whether the values are meant to be used one at a time or combined together as flags. Both ambiguities matter in practice: unpredictable underlying storage size makes an enum unsafe to use in structures or APIs that need a guaranteed size, and the missing intent means two completely different usage patterns (mutually exclusive state vs. combinable flags) look identical in source code.
NS_ENUM and NS_OPTIONS solve both problems the same way: each macro takes an explicit backing type as its first argument, which fixes the exact storage size regardless of platform or compiler, and each macro's name itself documents the intent -- NS_ENUM says 'exactly one of these at a time,' NS_OPTIONS says 'any combination of these, combined with bitwise OR.' Both expand, under the hood, to an ordinary C enum plus some typedef machinery; the difference between them is entirely about the promise the API makes to its callers, not about any change in the underlying C construct.
This distinction is not just documentation -- it is what determines correct usage on the calling side. Treating an NS_ENUM value as combinable (OR-ing two of its cases together) produces a nonsensical, unintended value, since mutually exclusive states were never designed to overlap; treating an NS_OPTIONS value as if only one flag could ever be set at a time misses the entire point of a bitmask API and usually leads to code that silently ignores legitimate combinations a caller might pass in.
• Plain C enum leaves the underlying storage size unspecified and says nothing about whether values are meant to combine. • NS_ENUM(Type, Name): a fixed-size enumeration of mutually exclusive values -- exactly one at a time. • NS_OPTIONS(Type, Name): a fixed-size bitmask enumeration of combinable flag values -- any subset, OR'd together. • The choice between them documents API intent directly in the type and determines correct calling-side usage.
NS_ENUM: Modeling Mutually Exclusive State
NS_ENUM is the right tool whenever a value represents exactly one state out of a fixed set at any given moment -- a download is either idle, running, completed, or failed, never more than one of those simultaneously. The DownloadState example below is the canonical shape of an NS_ENUM declaration: an NSInteger backing type, a descriptive type name, and a comma-separated list of cases, each conventionally prefixed with the type name so that autocomplete groups them together and so that two unrelated enumerations never accidentally share a bare case name.
Internally, each case in an NS_ENUM still gets an ordinary integer value -- DownloadStateIdle is 0, DownloadStateRunning is 1, and so on upward, unless a case is given an explicit value -- but that numeric value is rarely meaningful on its own the way an NS_OPTIONS value's bit position is; it exists mainly so the compiler has something concrete to store and compare, and so switch statements can exhaustively cover every case.
Because NS_ENUM values are mutually exclusive by design, the natural way to consume one is a switch statement covering every case, letting the compiler warn if a case is ever added later and accidentally left unhandled -- a benefit that plain C enum provides too, but which reads far more clearly once the enumeration's fixed backing type and intent are spelled out via NS_ENUM.
• Use NS_ENUM whenever a value represents exactly one state from a fixed set, never a combination. • The DownloadState example: NSInteger backing type, cases prefixed with the type name by convention. • Each case still has an ordinary integer value under the hood, but that value is rarely meaningful on its own. • Switch statements are the natural, exhaustive way to consume an NS_ENUM value.
NS_OPTIONS: Modeling Combinable Bitmask Flags
NS_OPTIONS is the right tool whenever multiple independent settings can all be true at once, and a caller should be able to specify any subset of them in a single value -- exactly the PermissionOptions example below, where a caller might legitimately need read access alone, write access alone, or read and write and share all together. Each case in an NS_OPTIONS declaration is deliberately given an explicit value that is a distinct power of two -- 1 << 0, 1 << 1, 1 << 2, and so on -- so that every case occupies exactly one bit position, and no two cases can ever collide when combined.
This is the detail that makes NS_OPTIONS fundamentally different from NS_ENUM under the hood, even though both are declared with a nearly identical macro shape: an NS_ENUM's cases are usually sequential integers (0, 1, 2, 3, ...) meant to be compared with equality, while an NS_OPTIONS's cases are bit flags (1, 2, 4, 8, ...) meant to be combined and inspected with bitwise operators. Mixing the two mental models -- comparing an NS_OPTIONS value with == the way you would an NS_ENUM value, for instance -- is one of the most common mistakes this lesson covers.
The NSUInteger backing type in the PermissionOptions example, rather than NSInteger, is also a deliberate convention: bitmask values are logically unsigned quantities, and using an unsigned backing type avoids any ambiguity about how the highest bit is interpreted if a bitmask ever grows to use it.
• Use NS_OPTIONS whenever multiple independent settings can be combined into a single value at once. • The PermissionOptions example: each case is an explicit power of two (1 << 0, 1 << 1, 1 << 2), giving every flag its own bit. • NS_ENUM cases are typically sequential integers compared with ==; NS_OPTIONS cases are bit flags combined with bitwise operators. • NSUInteger is the conventional backing type for NS_OPTIONS, since a bitmask is logically an unsigned quantity.
Combining Flags with Bitwise OR
Combining two or more NS_OPTIONS flags into a single value uses the bitwise OR operator, |, which sets a bit in the result wherever that bit is set in either operand. PermissionOptionRead | PermissionOptionWrite produces a single NSUInteger value with both the read bit and the write bit set simultaneously -- 0001 OR'd with 0010 yields 0011 -- and that combined value can be stored in one variable, passed as one argument, and later inspected to recover exactly which flags were included.
This is precisely why each case needed a distinct power-of-two value in the first place: because no two flag values share a bit position, OR-ing any combination of them together always produces a unique result with exactly the intended bits set, and no combination of flags can ever accidentally produce the same numeric value as a different, unrelated combination.
• Bitwise OR (|) combines flags: the result has a bit set wherever either operand has that bit set. • PermissionOptionRead | PermissionOptionWrite combines two distinct bit positions into one value with both bits set. • Distinct power-of-two values guarantee every possible combination of flags produces a unique, unambiguous result.
Checking a Flag with Bitwise AND
Checking whether a specific flag is present inside a combined NS_OPTIONS value uses the bitwise AND operator, &, together with a comparison against the flag itself: (options & PermissionOptionWrite) != 0 (or, equivalently and idiomatically, (options & PermissionOptionWrite) == PermissionOptionWrite) isolates just the write bit from whatever other bits are also set in options, leaving a nonzero result only if that specific bit was actually present.
This is the single most common mistake beginners make with NS_OPTIONS: writing if (options == PermissionOptionWrite) instead, which only evaluates true if options is exactly the write flag alone and nothing else -- it silently fails for any caller who legitimately combined write with read or share, since the combined value is numerically different from the write flag by itself. The & check is correct precisely because it isolates one bit regardless of what else is set alongside it.
• Checking a flag: (options & PermissionOptionWrite) != 0 -- isolates just that bit, ignoring every other bit that may also be set. • Equivalent, more explicit form: (options & PermissionOptionWrite) == PermissionOptionWrite. • Using == instead of & is the classic mistake: it only matches when that flag is the only one set, breaking on any legitimate combination.
Clearing a Flag with AND-NOT
Removing one specific flag from a combined value, while leaving every other flag exactly as it was, uses bitwise AND combined with bitwise NOT: options &= ~PermissionOptionWrite first inverts every bit of PermissionOptionWrite with ~, producing a mask with every bit set except the write bit, and then ANDs that mask against options, which clears the write bit specifically while leaving every other bit in options completely untouched.
This pattern -- variable &= ~flag; -- is worth memorizing as a unit, because getting the operator precedence or the negation wrong is easy: forgetting the ~ entirely and writing options &= PermissionOptionWrite does something completely different (it clears every flag except write, the opposite of the intended effect), which is exactly the kind of subtle bitmask mistake that compiles cleanly and fails silently.
• Clearing a flag: options &= ~PermissionOptionWrite -- inverts the flag's bit pattern, then ANDs it against options to clear just that bit. • Every other bit in options is left completely unchanged by this operation. • Forgetting the ~ (writing options &= PermissionOptionWrite) clears every flag except the one intended -- the exact opposite of clearing it.
How NS_ENUM and NS_OPTIONS Import Differently Into Swift
The Swift importer treats NS_ENUM and NS_OPTIONS completely differently, precisely because each macro documents a different intent, and Swift has a native language construct matching each one. An Objective-C type declared with NS_ENUM imports into Swift as an ordinary Swift enum, with each case becoming a case of that enum (typically with the shared type-name prefix stripped, since Swift's own namespacing makes it redundant) -- DownloadStateRunning in Objective-C becomes .running in Swift, used with Swift's exhaustive switch statement exactly the way the enum was meant to be consumed in Objective-C.
An Objective-C type declared with NS_OPTIONS, by contrast, imports into Swift as a type conforming to Swift's OptionSet protocol, not as a plain enum -- because Swift's own enum cases cannot be combined with bitwise operators the way Objective-C's NS_OPTIONS cases can, and OptionSet is Swift's dedicated construct for exactly this bitmask-combination pattern. In Swift, combining flags looks like [.read, .write] (an array-literal-style set expression) rather than Objective-C's PermissionOptionRead | PermissionOptionWrite, and checking a flag uses OptionSet's own .contains(.write) method rather than a manual bitwise AND.
This importer behavior is exactly why choosing NS_ENUM versus NS_OPTIONS correctly on the Objective-C side matters even to teams that never write Objective-C call sites themselves: get it right, and Swift code calling into the API reads idiomatically and safely in Swift's own terms; get it backwards -- declaring combinable flags as NS_ENUM, for instance -- and Swift code loses the ability to combine or safely check flags the way OptionSet would have allowed.
• NS_ENUM imports into Swift as a native Swift enum -- DownloadStateRunning becomes .running, used with Swift's switch. • NS_OPTIONS imports into Swift as a type conforming to OptionSet, not a plain enum, since Swift enum cases cannot be bitwise-combined. • Swift combines OptionSet flags with a set literal like [.read, .write] and checks them with .contains(.write), not with | and &. • Choosing the wrong macro on the Objective-C side directly degrades how the API can be used correctly and idiomatically from Swift.
API Design Guidance
The practical rule this lesson leaves you with is simple to state and easy to apply while designing a new Objective-C type: ask whether two or more values could ever legitimately be true about the same thing at the same time. If the answer is no -- a download cannot be both idle and running -- the states are mutually exclusive and NS_ENUM is correct. If the answer is yes -- a user can legitimately have read and write and share permission simultaneously -- the values are combinable flags and NS_OPTIONS, with power-of-two case values, is correct.
Getting this decision right up front pays off in three places at once, each covered above: it determines the fixed storage size the compiler uses regardless of platform, it determines whether calling code should use == and switch or | / & / &~, and it determines whether Swift sees a native enum or an OptionSet -- one small, upfront design decision that ripples through the entire lifetime of the API on both sides of the Objective-C/Swift boundary.
• Ask: can two or more values be true about the same thing simultaneously? No -- NS_ENUM. Yes -- NS_OPTIONS with power-of-two cases. • The decision determines storage size, correct bitwise-vs-equality usage on the calling side, and Swift's imported representation. • This single design decision, made once at declaration time, shapes how the API is used correctly for its entire lifetime.
NS_ENUM and NS_OPTIONS Declarations
NS_ENUM(Type, Name): mutually exclusive values, typically sequential integers, consumed with ==/switch. NS_OPTIONS(Type, Name): combinable bitmask values, each an explicit power of two, consumed with | / & / &~.
typedef NS_ENUM(NSInteger, DownloadState) {
DownloadStateIdle,
DownloadStateRunning,
DownloadStateCompleted,
DownloadStateFailed
};
typedef NS_OPTIONS(NSUInteger, PermissionOptions) {
PermissionOptionRead = 1 << 0,
PermissionOptionWrite = 1 << 1,
PermissionOptionShare = 1 << 2
};Code Examples
DownloadState models exactly one state at a time, so a switch statement is the natural way to consume it -- the compiler can warn if a future case is ever added and left unhandled here. Each case is prefixed with DownloadState so autocomplete groups all four cases together and no other enumeration in the codebase can accidentally reuse a bare name like Idle.
switch (state) { Each case is a distinct power of two -- 1 (0001), 2 (0010), 4 (0100) -- so OR-ing any combination together always produces a unique value with exactly the intended bits set. userPermissions ends up as 0011 (3); adminPermissions ends up as 0111 (7), matching the diagram's Read + Write + Share combination exactly.
PermissionOptions userPermissions = PermissionOptionRead | PermissionOptionWrite;
(permissions & PermissionOptionWrite) isolates just the write bit out of whatever combination of flags is actually set, so canWrite: correctly returns YES for userPermissions even though userPermissions also has the read bit set. The commented-out == comparison demonstrates exactly why equality is the wrong tool: it only matches the exact single-flag value, not any combination that happens to include that flag.
return (permissions & PermissionOptionWrite) != 0;
~PermissionOptionWrite inverts every bit of the write flag's pattern, producing a mask with every bit set except the write bit (...1101); ANDing that mask against permissions clears only the write bit and leaves read and share completely untouched. The result, 0101, still has the read bit (0001) and share bit (0100) set, exactly as the follow-up checks confirm.
permissions &= ~PermissionOptionWrite;
This is the direct Swift-side result of the two macro choices covered in this lesson: DownloadState, declared with NS_ENUM, is usable with Swift's native switch exactly like any Swift enum. PermissionOptions, declared with NS_OPTIONS, is usable with Swift's set-literal syntax and OptionSet methods like contains(_:) and subtracting(_:), which correspond directly to Objective-C's |, &, and &~ operators.
if userPermissions.contains(.write) {
typedef NS_ENUM(NSInteger, DownloadState) {
DownloadStateIdle,
DownloadStateRunning,
DownloadStateCompleted,
DownloadStateFailed
};
- (NSString *)descriptionForState:(DownloadState)state {
switch (state) {
case DownloadStateIdle:
return @"Idle";
case DownloadStateRunning:
return @"Running";
case DownloadStateCompleted:
return @"Completed";
case DownloadStateFailed:
return @"Failed";
}
}typedef NS_OPTIONS(NSUInteger, PermissionOptions) {
PermissionOptionRead = 1 << 0,
PermissionOptionWrite = 1 << 1,
PermissionOptionShare = 1 << 2
};
// A user with both read and write access, but not share access:
PermissionOptions userPermissions = PermissionOptionRead | PermissionOptionWrite;
// A user with all three:
PermissionOptions adminPermissions =
PermissionOptionRead | PermissionOptionWrite | PermissionOptionShare;- (BOOL)canWrite:(PermissionOptions)permissions {
return (permissions & PermissionOptionWrite) != 0;
}
PermissionOptions userPermissions = PermissionOptionRead | PermissionOptionWrite;
if ([self canWrite:userPermissions]) {
NSLog(@"User has write access"); // this runs
}
// WRONG in comparison -- shown for contrast, not for use:
// if (userPermissions == PermissionOptionWrite) { ... } // false here!
// userPermissions is Read|Write combined (0011), not Write alone (0010),
// so a plain == comparison misses this legitimate combination entirely.PermissionOptions permissions =
PermissionOptionRead | PermissionOptionWrite | PermissionOptionShare; // 0111
// Revoke write access only, leaving read and share exactly as they were:
permissions &= ~PermissionOptionWrite; // 0111 & ~0010 = 0111 & 1101 = 0101
NSLog(@"Can still read: %@", (permissions & PermissionOptionRead) ? @"YES" : @"NO"); // YES
NSLog(@"Can still write: %@", (permissions & PermissionOptionWrite) ? @"YES" : @"NO"); // NO
NSLog(@"Can still share: %@", (permissions & PermissionOptionShare) ? @"YES" : @"NO"); // YES// DownloadState (NS_ENUM) imports as a native Swift enum:
let state: DownloadState = .running
switch state {
case .idle: print("Idle")
case .running: print("Running")
case .completed: print("Completed")
case .failed: print("Failed")
}
// PermissionOptions (NS_OPTIONS) imports as an OptionSet:
let userPermissions: PermissionOptions = [.read, .write]
if userPermissions.contains(.write) {
print("User has write access")
}
let reduced = userPermissions.subtracting(.write)Permission Flags as Bits
Each NS_OPTIONS case occupies one distinct bit position, so any combination of flags OR'd together produces a unique value -- 0111 unambiguously means all three flags are present at once.
PermissionOptions permissions = PermissionOptionRead | PermissionOptionWrite; if (permissions == PermissionOptionWrite) { // grant write-only behavior }
PermissionOptions permissions = PermissionOptionRead | PermissionOptionWrite; if (permissions & PermissionOptionWrite) { // grant write behavior, regardless of what else is combined in }
== only matches when PermissionOptionWrite is the single, exact value of permissions, which fails the moment any other flag is legitimately combined with it. & isolates just the bit being checked, so it correctly recognizes the write flag no matter what other flags are also present in the combined value.
permissions &= PermissionOptionWrite; // intended: clear write access only permissions &= ~PermissionOptionWrite; // clears write access only
Forgetting the ~ inverts nothing, so this ANDs permissions directly against the write flag alone -- the result keeps only the write bit (if it was set) and clears every other flag, which is the exact opposite of the intended 'remove write, keep everything else' behavior.
What is the primary difference in intent between NS_ENUM and NS_OPTIONS? A. NS_ENUM is for Swift-only code and NS_OPTIONS is for Objective-C-only code B. NS_ENUM models mutually exclusive values (exactly one at a time); NS_OPTIONS models combinable bitmask flags (any subset, OR'd together) C. NS_OPTIONS is simply a faster-compiling version of NS_ENUM with no semantic difference D. NS_ENUM requires an NSUInteger backing type, while NS_OPTIONS requires NSInteger
B. NS_ENUM models mutually exclusive values (exactly one at a time); NS_OPTIONS models combinable bitmask flags (any subset, OR'd together) — NS_ENUM documents that a value is exactly one state from a fixed set at any given time, while NS_OPTIONS documents that a value can be any combination of independent flag bits, combined with bitwise OR -- the macro name itself communicates the intended usage pattern.
Given PermissionOptions p = PermissionOptionRead | PermissionOptionShare;, which expression correctly checks whether p includes write access? A. p == PermissionOptionWrite B. p & PermissionOptionWrite C. p + PermissionOptionWrite D. p != PermissionOptionWrite
B. p & PermissionOptionWrite — Bitwise AND isolates just the bit being asked about, ignoring whatever other bits happen to also be set in p. == would only be true if p were exactly PermissionOptionWrite and nothing else, which is not the general case for a combined bitmask value.
What does options &= ~PermissionOptionWrite; do? A. Sets the write flag, leaving every other flag as it was B. Clears every flag except the write flag C. Clears the write flag specifically, leaving every other flag in options unchanged D. Toggles the write flag on or off, whichever it currently is not
C. Clears the write flag specifically, leaving every other flag in options unchanged — ~PermissionOptionWrite inverts the write flag's bit pattern, producing a mask with every bit set except the write bit; ANDing that mask against options clears only the write bit while every other bit in options passes through unchanged.
How does an Objective-C type declared with NS_OPTIONS typically import into Swift? A. As a plain Swift Int, with no type safety at all B. As a native Swift enum, identical to how NS_ENUM imports C. As a type conforming to Swift's OptionSet protocol, since Swift enum cases cannot be bitwise-combined D. NS_OPTIONS types cannot be used from Swift at all
C. As a type conforming to Swift's OptionSet protocol, since Swift enum cases cannot be bitwise-combined — Because Swift's own enum cases cannot be combined with bitwise operators, the Swift importer maps NS_OPTIONS types to OptionSet instead of to a plain enum -- OptionSet is Swift's native construct for exactly this bitmask-combination pattern, used with set-literal syntax and methods like contains(_:).
Why is each case in an NS_OPTIONS declaration given an explicit power-of-two value (1 << 0, 1 << 1, 1 << 2, ...) instead of the default sequential values (0, 1, 2, ...)? A. Power-of-two values compile faster than sequential integer values B. Each case must occupy a distinct bit position so that any combination of flags OR'd together produces a unique, unambiguous value with exactly the intended bits set C. It is purely a stylistic convention with no functional effect on how the flags behave D. Power-of-two values are required for NSUInteger but not for NSInteger
B. Each case must occupy a distinct bit position so that any combination of flags OR'd together produces a unique, unambiguous value with exactly the intended bits set — If two cases shared a bit position, combining them with | could produce the same numeric result as some other, unintended combination. Giving each case its own distinct power-of-two bit guarantees every possible OR'd combination is unique and can be checked or cleared independently with & and &~.
Declare an NS_OPTIONS type NotificationOptions with three flag cases -- NotificationOptionSound, NotificationOptionBadge, NotificationOptionBanner -- each a distinct power of two. Then write a function that takes a combined NotificationOptions value and returns YES only if both sound and badge are enabled, regardless of whether banner is also set, and a second function that returns a new value with banner cleared while preserving whatever else was set.
typedef NS_OPTIONS(NSUInteger, NotificationOptions) { NotificationOptionSound = 1 << 0, NotificationOptionBadge = 1 << 1, NotificationOptionBanner = 1 << 2 }; - (BOOL)hasSoundAndBadge:(NotificationOptions)options { NotificationOptions required = NotificationOptionSound | NotificationOptionBadge; return (options & required) == required; } - (NotificationOptions)clearingBanner:(NotificationOptions)options { return options & ~NotificationOptionBanner; }
Checking two flags at once combines the two case values with | before ANDing, and compares against that same combined mask: (options & (FlagA | FlagB)) == (FlagA | FlagB). Clearing one flag uses &= ~ exactly as shown in this lesson's clearing example.
0001 → Read
0010 → Write
0100 → Share
0111 → Read + Write + Share