NS_ENUM vs NS_CLOSED_ENUM for internal app code — how do you approach it?

I've been thinking about the right way to use NS_ENUM vs NS_CLOSED_ENUM for Objective-C enums in a large iOS codebase. Our codebase is a mixture of ObjC and Swift modules, with enums often defined in ObjC and consumed in Swift. This is internal app code, not a public library/framework. I'd love to hear how others approach this.

My initial reading

After reading SE-0192, I came away thinking NS_CLOSED_ENUM is safe to use broadly in internal code, since the frozen/non-frozen distinction was introduced primarily to address binary compatibility and library evolution. The proposal explicitly states:

"the core team has made it clear that such a distinction is only worth it for libraries that have binary compatibility concerns"

For internal code that always recompiles together, binary compatibility isn't a concern — when a case is added, all consumers are recompiled.

Apple's documentation seems to support this...

The Foundation Release Notes also frames it around binary compatibility:

"Once an enumeration is marked as closed, it's a binary- and source-incompatible change to add a new value."

...but another Apple doc takes a different angle

Grouping Related Objective-C Constants takes a more conceptual approach:

Use the NS_CLOSED_ENUM macro for a simple group of constants that you can never add new cases to. Closed enumerations are useful for representing a finite set of states...

Don't use the NS_CLOSED_ENUM macro if:

  • You've ever added cases to an enumeration after its initial declaration
  • You can think of additional cases you might add later
  • The enumeration has any private cases"

This framing is less about binary compatibility and more about semantic accuracy — if an enum isn't a "mathematically complete" finite set, don't call it closed.

My question

For those working on large iOS codebases with a mixture of ObjC and Swift modules (where ObjC-defined enums are frequently consumed by Swift code):

  1. Do you use NS_CLOSED_ENUM broadly for internal enums (prioritizing Swift ergonomics and avoiding @unknown default boilerplate)?
  2. Or do you stick with NS_ENUM for anything that might grow (prioritizing conceptual clarity)?
  3. Have you run into any practical issues with either approach at scale?

I'm genuinely trying to understand the tradeoffs here, not advocate for either side. Thanks!

2 Likes