How do you make an Objective-C API feel native in Swift?

4.5.3 · L3 · Interview

Predict first

What does this print?

print(Calc().total(items: 3))
  • 3
  • 6
  • Compile error

Predict first

What does this print?

print(Level.high.rawValue)
  • 1
  • 0
  • Compile error

Mental model. Subtitles for a film: the story is unchanged, but the audience does not have to read the original language.

Swift imports Objective-C by mechanical rules, which can produce awkward names. You can shape the result from the header. NS_SWIFT_NAME(total(items:)) renames a method, property or type for Swift only. NS_REFINED_FOR_SWIFT imports it with a leading __ so you can wrap it in a nicer Swift extension. NS_SWIFT_UNAVAILABLE("use X") hides a method that has a better Swift equivalent. NS_ENUM and NS_OPTIONS import as real Swift enums and option sets, with the common prefix removed from the cases (LevelHigh becomes .high), and NS_CLOSED_ENUM tells Swift the enum will never gain cases so a switch needs no default.

The goal is that a Swift caller never has to know the API came from Objective-C. For third-party headers you cannot edit, API notes files (.apinotes) apply the same renames and nullability without touching the source. Good naming is what makes a mixed codebase pleasant, and it costs almost nothing to add while you are already annotating nullability.

NS_SWIFT_NAME(total(items:)) gives the method its Swift name. The implementation doubles its argument.

NS_ENUM imports as a Swift enum with the Level prefix stripped, so LevelHigh is .high, with raw value 1.

Common traps

  • Renaming in Swift with a wrapper but leaving the ugly name public. Mark the original NS_SWIFT_UNAVAILABLE or refined.
  • Forgetting NS_ENUM and using a plain enum. It imports as loose integer constants, with no type safety.

They might follow up with

  • How are cases named? The common prefix shared with the type name is dropped and the rest is lower-camel-cased.
  • What are API notes for? Applying Swift names and annotations to a header you cannot modify.

Related questions

  • 4.5.1 How do nullability annotations change what Swift sees, and what happens when they lie?
  • 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?

← What are lightweight generics, and what do they not do?