Swift API Customization
Learn the full set of Objective-C annotations and conventions — NS_SWIFT_NAME, NS_REFINED_FOR_SWIFT, NS_SWIFT_UNAVAILABLE, nullability, instancetype, NS_ENUM/NS_OPTIONS, and NSError conventions — that let you shape exactly how an Objective-C API is imported and experienced from Swift.
Every Objective-C header that ships in a framework consumed from Swift goes through an automatic translation step called the Swift importer. Left alone, the importer does a reasonably good job: it strips redundant type prefixes, turns trailing NSError ** parameters into throws, and maps nullable/nonnull annotations onto Swift's Optional type. But 'reasonably good' is often not good enough for a public API — the importer's default guesses can produce Swift call sites that are verbose, redundant, or simply awkward, and an earlier module on API Design for Swift introduced the basic idea that Objective-C code can be written with a Swift consumer in mind. This addendum lesson goes much further than that introduction: it walks through every major annotation Apple provides for controlling the import process directly — NS_SWIFT_NAME, NS_REFINED_FOR_SWIFT, NS_SWIFT_UNAVAILABLE, and the interaction between nullability, instancetype, NS_ENUM, NS_OPTIONS, and NSError conventions — so that by the end you can deliberately design an Objective-C header that imports into Swift exactly the way you intend, rather than however the importer happens to guess.
The Swift Importer and Why Customization Matters
When a Swift file imports an Objective-C module, it does not see the Objective-C header verbatim. It sees the result of a translation pass — the Swift importer — that rewrites Objective-C declarations into their closest Swift equivalent, following a set of largely mechanical rules. A class named UIButton stays UIButton. A method named -setTitle:forState: becomes a Swift func named setTitle(_:for:), with the redundant 'title' and 'state' words folded into argument labels where the importer can infer them from the Objective-C selector's own piece names. Constants sharing a common prefix, like UIViewAnimationOptionCurveEaseIn, get that prefix stripped and become nested under an enum-like type, showing up in Swift as .curveEaseIn.
This automatic behavior is genuinely useful and, for the vast majority of straightforward APIs, produces a perfectly idiomatic Swift-facing surface without the Objective-C author lifting a finger beyond following normal naming conventions. But the importer's heuristics are not infallible, and they are especially likely to produce awkward results in three situations: selectors whose piece names don't cleanly separate into a base name and argument labels, methods that would be more natural to expose to Swift with an entirely different shape than their Objective-C signature literally has, and APIs that make sense in an Objective-C-only context but should not be exposed to Swift at all, or should be exposed differently.
This is exactly the gap the annotations in this lesson fill. Each one lets the Objective-C author override a specific piece of the importer's default behavior — the name it chooses, whether it imports at all, or whether the literal signature is replaced by an alternate Swift-only overlay — while leaving the underlying Objective-C implementation, and every Objective-C caller of that same API, completely untouched. Swift API customization is therefore purely additive: you are never rewriting your Objective-C implementation to please Swift, only attaching metadata that tells the importer how to present what is already there.
NS_SWIFT_NAME: Renaming the Import
NS_SWIFT_NAME is the most commonly used customization macro, and it does exactly what its name suggests: it overrides the name the Swift importer assigns to a declaration, whether that declaration is a method, a class, a function, a constant, or an entire enum case. You attach it directly to the Objective-C declaration, and it accepts a string written in Swift syntax describing exactly what the Swift-facing signature should look like.
The canonical example, and the one used throughout Apple's own frameworks, is a completion-handler method whose Objective-C selector doesn't naturally collapse into a clean Swift argument-label sequence. Consider a method declared as -fetchUserWithCompletion:(void (^)(User *user, NSError *error))completion. Left to its own devices, the importer would produce something like func fetchUser(withCompletion completion: @escaping (User?, Error?) -> Void) — grammatically fine, but noticeably more verbose than idiomatic Swift API design guidelines call for, which favor completion as the label rather than withCompletion. Attaching NS_SWIFT_NAME(fetchUser(completion:)) to the Objective-C declaration tells the importer to ignore its own heuristic entirely and use exactly the signature given in the macro's argument instead.
Crucially, NS_SWIFT_NAME changes nothing about the Objective-C side. Objective-C callers still write [self fetchUserWithCompletion:^(User *user, NSError *error) { ... }] exactly as before — the selector, the implementation, and every existing Objective-C call site are completely unaffected. Only the name Swift sees changes. This is what makes NS_SWIFT_NAME safe to add to any existing API at any time: it is purely a presentation-layer change for one specific consuming language.
NS_REFINED_FOR_SWIFT: Wrapping a Raw API with a Nicer Overlay
NS_SWIFT_NAME can rename a signature, but it cannot change its shape in ways that go beyond renaming — it cannot, for instance, turn an NSArray of NSNumber wrapped integers into a Swift [Int], or turn a C-style out-parameter pattern into a return value. For that level of transformation, Objective-C provides NS_REFINED_FOR_SWIFT, a macro that takes a different approach entirely: rather than renaming the imported declaration, it demotes the raw Objective-C declaration to a private, prefixed name in Swift (by convention prefixed with two underscores, like __fetchScoresRaw()) and expects you to write a genuinely separate, Swift-only overlay function or method, in an actual Swift file, that calls the raw underscore-prefixed version internally and exposes a nicer public Swift surface in its place.
The workflow looks like this: mark the raw Objective-C method with NS_REFINED_FOR_SWIFT, which makes it importable only under its underscore-prefixed name. Then, in a Swift extension shipped alongside the framework, write a method with the friendly name that Swift callers should actually use, whose body simply calls the underscore-prefixed raw version and adapts its result — unwrapping an NSNumber-filled array into native Int values, converting an NSError ** out-parameter pattern into a proper throws function, or turning a BOOL success flag plus a separate error parameter into a Swift Result type. Swift callers only ever see and use the refined overlay; the raw underscore-prefixed version is technically still there and technically still callable, but its ugly name signals clearly that it is not the intended entry point.
This two-layer approach exists precisely because some transformations are impossible to express as a pure rename. Renaming only changes what a signature is called; NS_REFINED_FOR_SWIFT lets you change what the signature actually looks like and how its types are represented, while still keeping a single Objective-C implementation underneath serving both the raw underscore-prefixed entry point and, indirectly, the polished Swift-only overlay built on top of it.
NS_SWIFT_UNAVAILABLE and Availability Annotations
Not every Objective-C API should be visible to Swift at all. Some methods exist purely to support Objective-C-specific patterns — manual memory management helpers left over from pre-ARC code, methods that only make sense alongside Objective-C's dynamic typing (id-typed collections with no generics, for instance), or methods that have a strictly better Swift-native equivalent elsewhere in the same framework and would only confuse Swift callers by also showing up. NS_SWIFT_UNAVAILABLE(message) marks a declaration so the Swift importer refuses to import it at all — any Swift code that tries to reference it fails to compile, with the message string you supplied shown directly in Xcode's error output, ideally pointing the Swift developer toward whichever alternative they should use instead.
NS_SWIFT_UNAVAILABLE differs meaningfully from simply not exposing a header to Swift at all, because it operates at the level of an individual declaration rather than an entire file. A single Objective-C class can have most of its methods import normally into Swift while a handful of Objective-C-only convenience methods are individually hidden with NS_SWIFT_UNAVAILABLE — giving fine-grained control over exactly which slice of a class's surface area Swift consumers see, without needing to split the class across multiple headers.
Availability more broadly extends beyond just Swift-unavailability. The standard API_AVAILABLE and __attribute__((availability(...))) annotations, which control which OS versions and SDK versions an API is available on, interact with the Swift importer too — Swift automatically understands @available checks generated from these same Objective-C availability annotations, and calling a not-yet-available API without an appropriate #available or @available check produces a compile error in Swift, exactly mirroring the compile-time guard Objective-C itself would apply with API_AVAILABLE. Designing availability correctly in the Objective-C header therefore pays off automatically in both languages at once.
NSError Conventions and the throws Import
One of the Swift importer's most valuable automatic transformations concerns error handling. An Objective-C method that follows Apple's standard error convention — returning BOOL or an object pointer to indicate success/failure, and taking a trailing NSError * __autoreleasing * (in header syntax, NSError ) parameter that it fills in only on failure — is recognized by the importer and rewritten into a genuine Swift throws function, with the trailing NSError parameter removed from the Swift signature entirely and folded into Swift's native try/catch error-handling model instead.
For this transformation to trigger, the method must follow the convention precisely: the NSError ** parameter must be the last parameter, and the method's return type must signal failure independently of the error — either a BOOL that is NO on failure, or an object-pointer return type that is nil on failure. A method declared as - (BOOL)writeToFile:(NSString *)path error:(NSError **)error imports into Swift as func write(toFile path: String) throws, with no error parameter and no BOOL return value visible in Swift at all — Swift callers write try obj.write(toFile: path) and catch thrown errors with an ordinary do/catch block, exactly as if the method had been written in Swift as a throwing function from the start.
The completion-handler analogue of this same convention — a block parameter of type void (^)(ResultType *result, NSError *error) as the final parameter — is recognized by more recent Swift importer versions as an async-importable pattern too, letting Swift callers use it with await inside an async function instead of a completion-handler closure, provided the completion block follows the (value, error) or (value?, error?) shape consistently. Designing your completion-handler signatures to strictly follow this two-parameter, value-then-error convention is therefore not just a stylistic nicety — it's what unlocks Swift's async/await import path for that method for free.
Nullability, Optionals, and instancetype
Objective-C's nullability annotations — nullable, nonnull, and the audited-region syntax NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END covered in an earlier module — map directly onto Swift's Optional type during import. A property or parameter marked nullable imports as an Optional in Swift (User?), one marked nonnull imports as a non-optional (User), and any pointer type left completely unaudited imports as an implicitly unwrapped optional (User!), which is exactly why every modern Objective-C header intended for Swift consumption should be wrapped in NS_ASSUME_NONNULL_BEGIN/END with explicit nullable overrides only where nil truly is a valid value — leaving nullability unaudited pushes the ambiguity straight into Swift as an implicitly unwrapped optional, which defeats much of the safety Swift's type system is designed to provide.
instancetype plays a related but distinct role. Declared as the return type of an initializer or a class factory method in place of a literal class name, instancetype tells the compiler that the method returns whatever type it was actually called on — meaning a subclass's inherited initializer correctly returns the subclass's own type, not the base class's type. This matters enormously for Swift import because Swift's own initializers always behave this way natively; if an Objective-C initializer were declared to return the base class type literally instead of instancetype, Swift would import it as returning that exact base class, and calling it on a subclass would produce a value Swift believes is the base class even though at runtime it truly is the subclass — forcing awkward casts at every subclass call site. instancetype is therefore not just an Objective-C-side correctness detail; it's a prerequisite for Objective-C initializers to import into Swift with the fully-typed, cast-free behavior Swift developers expect as the baseline.
NS_ENUM and NS_OPTIONS at the Swift Boundary
NS_ENUM and NS_OPTIONS, covered in full detail in this addendum's sibling lesson on NS_ENUM and NS_OPTIONS, both give the Swift importer enough structural information to turn a set of loose Objective-C integer constants into a genuine Swift enum. NS_ENUM values, sharing a common prefix, become Swift enum cases with that prefix stripped, accessible with dot-syntax at the call site. NS_OPTIONS values — declared the same way but intended to be OR'd together as bitmask flags — import as a Swift OptionSet, which lets Swift callers combine them with array-literal syntax ([.curveEaseIn, .repeat]) and use set-style operations (.contains(), .insert()) instead of raw bitwise operators.
Declaring a set of related constants as a plain, un-macro'd sequence of static const NSInteger values, instead of wrapping them in NS_ENUM, denies the Swift importer the structural grouping information it needs — each constant imports as an isolated global value with no shared enum type connecting them, forcing Swift callers to work with bare integers and losing switch-statement exhaustiveness checking entirely. This is one of the clearest cases in this whole lesson where a purely Objective-C-side stylistic choice (NS_ENUM vs. a loose sequence of constants) has an outsized, directly visible effect on how good or bad the resulting Swift API feels, even though NS_ENUM long predates Swift and exists independently for good Objective-C-side reasons of its own — type safety and exhaustive switch-case warnings in Objective-C itself.
Designing an Objective-C API Specifically for Swift Consumers
Pulling every annotation in this lesson together, designing an Objective-C API with a Swift consumer specifically in mind comes down to a short discipline: wrap headers in NS_ASSUME_NONNULL_BEGIN/END and audit nullability deliberately rather than leaving it implicit; declare initializers and factory methods with instancetype rather than a literal class name; group related integer constants with NS_ENUM or NS_OPTIONS rather than loose constants; follow the NSError ** trailing-parameter convention (or the (value, error) completion-handler shape) precisely so error handling imports as throws or async; reach for NS_SWIFT_NAME whenever the importer's default guess produces an awkward or overly verbose Swift signature; reach for NS_REFINED_FOR_SWIFT when the ideal Swift shape genuinely diverges from what a rename alone can express; and use NS_SWIFT_UNAVAILABLE to hide Objective-C-only surface area that would only confuse or mislead a Swift caller.
None of these techniques require abandoning idiomatic Objective-C or writing your implementation any differently than you otherwise would — every one of them is metadata layered on top of an Objective-C declaration that otherwise behaves exactly as it always did for Objective-C callers. That is the essential insight of Swift API customization: it is not a compromise between two languages' conventions, but a translation layer that lets a single Objective-C implementation present two genuinely idiomatic faces, one to each language, at the same time.
Swift API Customization Annotations
NS_SWIFT_NAME accepts a full Swift-syntax signature string, including argument labels. The NSError ** parameter must be last, and the return type must independently signal failure (BOOL NO, or nil), for the throws import to trigger. instancetype is required, not just conventional, for correct subclass-returning initializers in Swift.
NS_ASSUME_NONNULL_BEGIN
@interface UserService : NSObject
// NS_SWIFT_NAME rewrites the imported signature entirely.
- (void)fetchUserWithCompletion:(void (^)(User *_Nullable user, NSError *_Nullable error))completion
NS_SWIFT_NAME(fetchUser(completion:));
// NSError ** trailing parameter imports as `throws` in Swift.
- (BOOL)saveUser:(User *)user error:(NSError **)error;
// instancetype ensures subclasses import with the correct Self type.
- (instancetype)initWithName:(NSString *)name;
// NS_SWIFT_UNAVAILABLE hides an Objective-C-only helper from Swift.
- (void)_legacyFlushCacheOnMainThread NS_SWIFT_UNAVAILABLE("Use flushCache() instead.");
@end
// NS_REFINED_FOR_SWIFT demotes the raw method to __fetchScoresRaw()
// so a Swift overlay (written separately, in Swift) can present a
// friendlier [Int] return type in its place.
- (NSArray<NSNumber *> *)fetchScores NS_REFINED_FOR_SWIFT;
NS_ASSUME_NONNULL_ENDCode Examples
Without NS_SWIFT_NAME, the importer would produce something like fetchUser(withCompletion:) from the selector pieces. The explicit annotation overrides that guess, producing the exact Swift signature fetchUser(completion:) instead — shorter, and matching Swift API design guidelines' preference for completion over withCompletion as an argument label.
The Swift call site shows both import paths available for a properly-shaped completion handler: the classic closure-based call using the NS_SWIFT_NAME-assigned label, and the newer async/await form the importer generates automatically because the completion block follows the (value, error) convention described in the NSError conventions section.
The raw Objective-C method still exists and still works exactly as written for Objective-C callers. NS_REFINED_FOR_SWIFT simply demotes its Swift-visible name so a hand-written Swift extension can present the nicer [Int] return type in its place, which is a transformation NS_SWIFT_NAME alone cannot express.
This method follows Apple's standard error convention exactly: the NSError ** parameter is last, and the BOOL return value independently signals success (YES) or failure (NO). The Swift importer recognizes this shape and rewrites the imported signature to drop both the BOOL and the error parameter, replacing them with Swift's own throws mechanism.
Note there is no BOOL and no NSError parameter anywhere in the Swift signature — both have been folded into the throws keyword and Swift's native error-propagation model. This is why designing Objective-C error-returning methods to follow the convention precisely, rather than approximately, matters: any deviation (error parameter not last, or a return type that doesn't independently signal failure) causes the importer to fall back to a literal, un-idiomatic import instead.
Swift code that attempts to call _legacyFlushCacheOnMainThread fails to compile, with Xcode showing the supplied message string directly at the call site — steering Swift developers toward the intended replacement API instead of letting them discover and depend on an Objective-C-only implementation detail.
- (void)fetchUserWithCompletion:(void (^)(User *user, NSError *error))completion
NS_SWIFT_NAME(fetchUser(completion:));userService.fetchUser(completion: { user, error in
if let error = error {
print("Fetch failed: \(error)")
return
}
print("Got user: \(user!)")
})
// Because the completion block follows the (value, error) shape,
// modern Swift importers also expose an async form:
let user = try await userService.fetchUser()// In Objective-C: - (NSArray<NSNumber *> *)fetchScores NS_REFINED_FOR_SWIFT;
// imports into Swift only as __fetchScoresRaw().
// A hand-written Swift overlay wraps it with a friendlier type:
extension UserService {
func fetchScores() -> [Int] {
return __fetchScoresRaw().map { $0.intValue }
}
}
// Swift callers only ever see and call the overlay:
let scores = userService.fetchScores() // [Int], not [NSNumber]- (BOOL)writeToFile:(NSString *)path error:(NSError **)error;func write(toFile path: String) throws
// Swift call site:
do {
try model.write(toFile: "/tmp/data.json")
} catch {
print("Write failed: \(error)")
}- (void)_legacyFlushCacheOnMainThread NS_SWIFT_UNAVAILABLE("Use flushCache() instead.");Important Rules
NS_SWIFT_NAME changes only what Swift sees — every existing Objective-C call site keeps working unmodified.
Use NS_REFINED_FOR_SWIFT, not NS_SWIFT_NAME, whenever the ideal Swift shape requires more than a rename — such as a different return type.
A method must have its NSError ** parameter last, with a BOOL or nullable-object return type, to import as throws.
Always declare initializers and factory methods with instancetype so Swift imports the correct, cast-free subclass type.
Wrap Swift-facing headers in NS_ASSUME_NONNULL_BEGIN/END rather than leaving nullability unaudited and implicitly-unwrapped.
Where This Shows Up
Nearly every Apple system framework with Objective-C roots — UIKit, Foundation, CoreData — ships with extensive NS_SWIFT_NAME and nullability annotations precisely so that Swift developers experience those frameworks as native-feeling Swift APIs rather than literal, mechanically-translated Objective-C. Any team maintaining a mixed Objective-C/Swift codebase, or an Objective-C framework distributed to Swift-using third parties, runs into these same annotations directly: a completion-handler API added without following the (value, error) convention loses async/await import for free; a factory method declared to return id instead of instancetype forces Swift callers into manual casts; and a set of loose integer constants instead of an NS_ENUM loses Swift's switch-exhaustiveness checking entirely.
What does NS_SWIFT_NAME(fetchUser(completion:)) change about the annotated Objective-C method? A. It changes the compiled Objective-C selector to match the new name B. It only changes the name the Swift importer assigns; Objective-C callers are unaffected C. It marks the method unavailable from Objective-C going forward D. It converts the method into a Swift-only function with no Objective-C implementation
B. It only changes the name the Swift importer assigns; Objective-C callers are unaffected — NS_SWIFT_NAME is purely a presentation-layer annotation for the Swift importer. The Objective-C selector, implementation, and every existing Objective-C call site remain completely unchanged.
When should you reach for NS_REFINED_FOR_SWIFT instead of NS_SWIFT_NAME? A. Whenever you just want to shorten a Swift method's argument labels B. When the ideal Swift-facing shape requires more than a rename, such as a different return type C. Whenever a method should be hidden from Swift entirely D. Only for initializers, never for ordinary instance methods
B. When the ideal Swift-facing shape requires more than a rename, such as a different return type — NS_REFINED_FOR_SWIFT demotes the raw declaration to an underscore-prefixed name and expects a hand-written Swift overlay to present a genuinely different shape — something a pure rename via NS_SWIFT_NAME cannot express.
For an Objective-C method to import into Swift as a throws function, where must its NSError parameter appear, and what must its return type do? A. The NSError can appear anywhere; the return type must be void B. The NSError must be the first parameter; the return type is irrelevant C. The NSError must be the last parameter, and the return type must independently signal failure (BOOL NO, or nil) D. The NSError ** must be wrapped in NS_SWIFT_NAME manually for the conversion to happen
C. The NSError must be the last parameter, and the return type must independently signal failure (BOOL NO, or nil) — The Swift importer only recognizes the throws-conversion pattern when the NSError parameter is last and the method's return type (BOOL or a nullable object) independently signals success or failure.
Why must an initializer be declared with instancetype rather than a literal class name for correct Swift import? A. instancetype is required syntax that Swift cannot parse otherwise B. Without instancetype, Swift would import the initializer as returning the base class type literally, forcing casts at subclass call sites C. instancetype makes the initializer import as throws automatically D. instancetype is only relevant for class (static) factory methods, never for -init methods
B. Without instancetype, Swift would import the initializer as returning the base class type literally, forcing casts at subclass call sites — instancetype tells the compiler an initializer returns whatever type it was actually called on. Declaring the base class name literally instead would make Swift import the initializer as always returning that exact base type, breaking the cast-free subclassing behavior Swift developers expect.
What is the effect of declaring a group of related integer constants with NS_ENUM instead of as loose static const NSInteger values, from a Swift-import perspective? A. There is no difference; both import identically into Swift B. NS_ENUM constants import as isolated global values just like loose constants C. NS_ENUM gives the importer the structural grouping it needs to produce a genuine Swift enum with dot-syntax and exhaustive switch checking D. NS_ENUM constants become unavailable in Swift entirely
C. NS_ENUM gives the importer the structural grouping it needs to produce a genuine Swift enum with dot-syntax and exhaustive switch checking — NS_ENUM's structural grouping is what lets the Swift importer produce a proper Swift enum type. Loose constants import as disconnected global values, losing dot-syntax access and switch-statement exhaustiveness checking.
What does NS_SWIFT_UNAVAILABLE do, and how does it differ from simply not exposing a header to Swift? A. It hides an entire header file from Swift, the same as excluding it from the module map B. It hides one specific declaration from Swift while leaving the rest of the same class's API importable normally C. It disables the method in Objective-C as well as Swift D. It is only usable on class declarations, never on individual methods
B. It hides one specific declaration from Swift while leaving the rest of the same class's API importable normally — NS_SWIFT_UNAVAILABLE operates at the level of an individual declaration, giving fine-grained control to hide just one Objective-C-only method or property while the rest of the same class continues to import into Swift normally.
Take an existing Objective-C method -(void)loadImageForURL:(NSURL *)url withCompletionBlock:(void (^)(UIImage *image, NSError *error))block and (1) add an NS_SWIFT_NAME annotation producing the Swift signature loadImage(for:completion:), and (2) explain, in one sentence, why this same method's completion block shape already qualifies it for Swift's async/await import path without any further annotation.
• (void)loadImageForURL:(NSURL *)url withCompletionBlock:(void (^)(UIImage *image, NSError *error))block NS_SWIFT_NAME(loadImage(for:completion:)); — and it already qualifies for async/await import because its completion block follows the standard (value, error) two-parameter convention the Swift importer looks for.
For part 2, look back at the NSError Conventions section's description of the (value, error) completion-handler shape.