Objective-C API Annotations

Learn the full set of professional API-design annotations Objective-C headers use to communicate compiler-enforced contracts -- nullability regions, required superclass calls, designated initializers, unavailable methods, and platform availability and deprecation markers -- and understand how each one affects both the Objective-C compiler and the Swift importer.

A well-designed Objective-C header does more than declare method signatures -- it encodes a set of promises and requirements directly into the type system, so that misuse is caught by the compiler rather than discovered at runtime or, worse, in production. NS_ASSUME_NONNULL_BEGIN/END, NS_REQUIRES_SUPER, NS_DESIGNATED_INITIALIZER, NS_UNAVAILABLE, and the API_AVAILABLE/API_DEPRECATED family of macros are the toolkit professional Objective-C API design relies on for exactly this purpose. Some of these annotations were previewed elsewhere -- the Nullability lesson covers NS_ASSUME_NONNULL_BEGIN/END in depth, and Initializer Design covers NS_DESIGNATED_INITIALIZER as part of a full treatment of initializer chains -- but this lesson is the first place every one of these annotations is gathered together as a single, professional API-design toolkit, and it is also the first place the availability and deprecation macros get dedicated coverage. Where a topic is covered in depth elsewhere, this lesson gives it a focused, self-contained example and points to the fuller treatment by name.

Why API Annotations Matter: Encoding Contracts Into the Type System

Every annotation in this lesson exists to convert an implicit expectation -- something a well-behaved caller or subclass is simply supposed to know -- into an explicit, compiler-checked contract. Without annotations, a header can only communicate intent through comments and documentation, both of which are easy to miss, easy to skim past, and impossible for the compiler to enforce. A comment saying 'do not pass nil here' does nothing to stop nil from being passed; a doc comment saying 'subclasses must call super' does nothing to stop a subclass from forgetting to.

Each annotation covered in this lesson closes exactly one such gap: nullability annotations make nil-safety checkable at compile time instead of just documented; NS_REQUIRES_SUPER at least warns when a required super call is missing; NS_DESIGNATED_INITIALIZER and NS_UNAVAILABLE make initializer misuse a compile error instead of a runtime surprise; and the availability and deprecation macros make platform-version mistakes and outdated API usage visible immediately in Xcode rather than discovered later on an unsupported OS version or after a symbol has actually been removed.

Together, these annotations are what separates a header that merely compiles from one that actively helps every caller and every subclass use the API correctly -- which is exactly why professional Objective-C frameworks, including Apple's own, use every annotation in this lesson extensively throughout their public headers.

• Annotations convert implicit, comment-only expectations into explicit, compiler-checked contracts. • Each annotation in this lesson closes one specific gap: nullability, required super calls, initializer correctness, or platform availability. • A well-annotated header actively helps callers and subclasses use an API correctly, rather than merely compiling successfully.

NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END

The Nullability lesson covers nullable, nonnull, and these two macros in full depth; this lesson's job is to place them in context alongside the rest of the API-annotation toolkit. NS_ASSUME_NONNULL_BEGIN marks the start of a region -- almost always spanning most or all of a header file -- where every unannotated pointer type is treated by the compiler as nonnull by default; NS_ASSUME_NONNULL_END marks where that default stops applying. Individual parameters or return values that genuinely can be nil are marked nullable explicitly within that region, as the exception to the region's default rather than the rule.

This pair of macros is almost always the very first annotation applied to a new, professionally designed header, precisely because it flips Objective-C's historical default -- where any pointer could silently be nil unless documented otherwise -- into the safer default of nonnull-unless-marked, which is also the default Swift already assumes for its own types, making Swift-imported signatures read naturally without extra optionals cluttering every parameter.

• NS_ASSUME_NONNULL_BEGIN / END: everything unannotated inside the region defaults to nonnull; nullable is the explicit exception. • Covered in full depth in the dedicated Nullability lesson -- this lesson places it as the first annotation in the professional toolkit. • Almost always the first annotation applied to a new header, since it establishes the safer default for everything else in the file.

NS_REQUIRES_SUPER: Enforcing a Required Superclass Call

NS_REQUIRES_SUPER, applied to a method declaration, tells both the compiler and any subclass author that overriding this method must call the superclass's implementation as part of the override -- typically because the superclass's own implementation performs setup or teardown work that the subclass's override depends on, directly or indirectly, even if the override's own logic looks self-contained. - (void)prepare NS_REQUIRES_SUPER; is exactly the spec's example: any subclass overriding prepare is expected to include [super prepare]; somewhere in its own implementation.

The compiler backs this annotation with a real warning -- not a silent expectation -- when an override is detected that does not call super anywhere in its implementation, which is meaningfully stronger than a comment alone, even though (as the mistakes section below covers) it stops short of being a hard compile error the way NS_UNAVAILABLE misuse is.

NS_REQUIRES_SUPER is most valuable on methods a base class uses for its own internal bookkeeping that a subclass has no way to replicate correctly on its own -- registering the object with some shared system, initializing internal state a superclass method later depends on, or, as in the example, running preparation logic a base class needs before subclass-specific preparation runs.

• NS_REQUIRES_SUPER: an override of this method must call the superclass implementation as part of its own. • - (void)prepare NS_REQUIRES_SUPER; means every override must include [super prepare]; somewhere in its body. • The compiler emits a genuine warning when an override omits the required super call -- stronger than a comment, though not a hard error. • Most valuable on methods that perform base-class bookkeeping a subclass has no other way to replicate correctly.

NS_DESIGNATED_INITIALIZER and NS_UNAVAILABLE: Controlling Initialization Paths

NS_DESIGNATED_INITIALIZER, applied to an init method, marks that initializer as the one every other initializer in the class must ultimately funnel through -- the full mechanics of designated versus convenience initializer chains, and the compiler warnings that result from an incomplete or inconsistent chain, are the entire subject of the sibling Initializer Design lesson. This lesson's job is only to place NS_DESIGNATED_INITIALIZER inside the broader annotation toolkit and show its most common companion pattern: pairing a designated initializer with NS_UNAVAILABLE on the class's inherited plain init, so that callers are steered toward the initializer that actually configures the object correctly.

NS_UNAVAILABLE, applied to any method declaration, tells the compiler that this method must never be called at all -- attempting to call a method marked NS_UNAVAILABLE is a hard compile error, not a warning, which is the strongest annotation covered in this lesson. Marking a class's plain, inherited init as NS_UNAVAILABLE is the standard way to force every caller through a designated initializer that actually requires the arguments the class needs to be constructed correctly, since plain init alone would otherwise still compile and silently produce a half-configured object.

NS_UNAVAILABLE is not limited to initializers -- it applies to any method a class wants to explicitly forbid, including one inherited from a superclass that simply does not make sense for this particular subclass, communicating that prohibition to the compiler rather than leaving it as a runtime assertion or a comment that a careless caller might miss entirely.

• NS_DESIGNATED_INITIALIZER: marks the one initializer every other initializer in the class must funnel through -- full chain mechanics live in the Initializer Design lesson. • NS_UNAVAILABLE: calling this method is a hard compile error, the strongest annotation in this lesson -- not merely a warning. • The common pairing: mark the inherited plain init as NS_UNAVAILABLE alongside a designated initializer that requires real configuration arguments. • NS_UNAVAILABLE applies to any method a class wants to forbid outright, not only initializers.

API_AVAILABLE: Marking Platform-Version Requirements

API_AVAILABLE(platform version, ...) marks a declaration -- a method, a property, a whole class -- as only available starting from a specific OS version on a specific platform, such as API_AVAILABLE(ios(13.0)) for something introduced in iOS 13. The compiler uses this annotation to flag any call site that might execute on an OS version older than the one specified, prompting either an availability check at runtime (using @available in Swift-adjacent code, or the equivalent Objective-C availability check) or raising the deployment target, rather than allowing code to silently call a symbol that does not exist yet on some device it might actually run on.

This annotation matters specifically because Objective-C code frequently supports a deployment target older than the newest SDK it is compiled against -- a project might have a deployment target of iOS 12 while building against an SDK that also includes iOS 13 and iOS 14 APIs, and API_AVAILABLE is what lets the compiler distinguish 'available in the SDK I'm compiling against' from 'actually safe to call unconditionally, given what this app promises to support.'

• API_AVAILABLE(ios(13.0)): this declaration is only available starting from iOS 13.0. • The compiler flags call sites that might run on an older OS version than the one specified, prompting an availability check. • Matters most when a project's deployment target is older than the newest SDK version it is compiled against.

API_DEPRECATED, API_DEPRECATED_WITH_REPLACEMENT, and API_UNAVAILABLE

API_DEPRECATED marks a declaration as deprecated starting from a given OS version, producing a compiler warning at every call site once code targets that version or later -- it communicates 'this still works, but you should stop relying on it,' without going as far as forbidding its use outright the way NS_UNAVAILABLE does for a method that should never be called at all.

API_DEPRECATED_WITH_REPLACEMENT extends the same idea with one more piece of information: the name of the API that should be used instead, which Xcode surfaces directly in the deprecation warning and, in many cases, offers as an automatic fix-it. This is the friendlier, more actionable form of deprecation, and it is the one professional frameworks favor whenever a direct replacement genuinely exists, since it tells the caller not just that something is wrong but exactly how to fix it.

API_UNAVAILABLE marks a declaration as entirely unavailable on a specific platform -- distinct from API_DEPRECATED, which still permits calling the deprecated symbol (with a warning); API_UNAVAILABLE means the symbol should not be called at all on that platform, which is useful for APIs that exist on one Apple platform (say, macOS) but were never meaningfully portable to another (say, watchOS), so that cross-platform code gets an immediate, clear compiler error rather than a link-time failure or an undefined-behavior crash on the unsupported platform.

• API_DEPRECATED(message, ios(9.0, 13.0)): still callable, but warns starting from the deprecation version -- 'stop relying on this.' • API_DEPRECATED_WITH_REPLACEMENT("newAPI", ios(9.0, 13.0)): the same warning, plus the exact replacement API, often with an automatic Xcode fix-it. • API_UNAVAILABLE(watchos): the symbol must not be called at all on that platform -- stronger than deprecation, an outright compile error there. • Deprecated still compiles with a warning; unavailable does not compile at all on the platform it names.

Availability Checks and API Annotations in Swift Importer Behavior

Every annotation in this lesson carries across the Swift importer, and each does so in a way that matches its Objective-C meaning closely. NS_ASSUME_NONNULL_BEGIN/END and per-parameter nullability determine whether a Swift parameter or return type is optional or non-optional, exactly as the Nullability lesson describes. NS_UNAVAILABLE-marked methods are simply not imported into Swift at all -- Swift code cannot call them because, as far as Swift's API surface is concerned, they do not exist. NS_DESIGNATED_INITIALIZER affects which Swift initializers a subclass is required to implement, mirroring the designated/convenience distinction the Initializer Design lesson covers on the Objective-C side.

API_AVAILABLE and its deprecation relatives import directly into Swift's own native availability system: an Objective-C declaration marked API_AVAILABLE(ios(13.0)) requires the exact same @available(iOS 13.0, *) guard (or an equivalent minimum deployment target) in Swift that a native Swift API introduced at the same version would require, and API_DEPRECATED / API_DEPRECATED_WITH_REPLACEMENT produce ordinary Swift deprecation warnings, including the same fix-it suggestion toward the replacement API when one was specified.

NS_REQUIRES_SUPER has no equivalent compiler enforcement in Swift for a Swift subclass overriding the method -- Swift does not have a directly matching required-super-call annotation, so an override written in Swift of an NS_REQUIRES_SUPER Objective-C method still depends on the Swift author following the same documented contract by hand, which is exactly why the discipline covered in this lesson's next section matters regardless of which language ends up overriding the method.

• Nullability annotations determine Swift optional vs. non-optional; NS_UNAVAILABLE methods are simply absent from Swift's API surface entirely. • NS_DESIGNATED_INITIALIZER shapes which initializers a Swift subclass must implement, mirroring Objective-C's own designated/convenience rules. • API_AVAILABLE and the deprecation macros map directly onto Swift's own @available system and deprecation warnings, fix-its included. • NS_REQUIRES_SUPER has no compiler-enforced Swift equivalent -- a Swift override still relies on the documented contract, not a compiler check.

NS_REQUIRES_SUPER

The compiler warns when an override of a NS_REQUIRES_SUPER method does not call [super ...] anywhere in its body. This is a warning, not a hard compile error -- unlike NS_UNAVAILABLE, which is enforced as a genuine compile error.

- (void)prepare NS_REQUIRES_SUPER;

// Every override must call the superclass implementation:
- (void)prepare {
    [super prepare];
    // subclass-specific preparation follows
}

Code Examples

Inside the region, name and bioText are treated as nonnull without writing anything extra -- passing nil to either is flagged by the compiler. avatarURLString is explicitly marked nullable because a user genuinely might not have an avatar set, making nil a legitimate, expected return value there rather than an oversight.

Task declares prepare with NS_REQUIRES_SUPER because its own implementation performs bookkeeping every subclass needs, even if a subclass's own preparation logic looks self-contained. UploadTask's override correctly calls [super prepare] first, so Task's base preparation still runs before UploadTask's own subclass-specific logic executes.

[super prepare]; // required -- Task's own preparation must still run initWithRadius: is the designated initializer -- the one path that actually configures a Circle correctly, as the full designated/convenience mechanics are covered in the sibling Initializer Design lesson. Marking the inherited plain init as NS_UNAVAILABLE makes calling it a hard compile error rather than allowing a half-configured Circle with an undefined radius to be constructed silently.

• (instancetype)init NS_UNAVAILABLE;

activateFocusMode is only available starting from iOS 13.0; because a project's deployment target might be older than that, the compiler requires an explicit availability check -- @available(iOS 13.0, *) -- before the call is allowed to compile unconditionally, exactly mirroring how the same API would need an @available guard from Swift.

• (void)activateFocusMode API_AVAILABLE(ios(13.0));

API_DEPRECATED_WITH_REPLACEMENT("fetchDataAsync", ios(9.0, 13.0)) tells the compiler that fetchDataWithCompletion: was introduced in iOS 9.0 and deprecated as of iOS 13.0, and names fetchDataAsync as exactly what callers should switch to -- Xcode surfaces that replacement name directly in the warning, which is the concrete advantage this macro has over plain API_DEPRECATED.

API_DEPRECATED_WITH_REPLACEMENT("fetchDataAsync", ios(9.0, 13.0));

NS_ASSUME_NONNULL_BEGIN

@interface UserProfile : NSObject

// Both parameters below are implicitly nonnull inside this region --
// no explicit annotation needed for the common case.
- (instancetype)initWithName:(NSString *)name
                     bioText:(NSString *)bioText;

// Explicitly marked nullable as the exception to the region's default:
- (nullable NSString *)avatarURLString;

@end

NS_ASSUME_NONNULL_END
@interface Task : NSObject
- (void)prepare NS_REQUIRES_SUPER;
@end

@implementation Task
- (void)prepare {
    // Base-class bookkeeping every subclass depends on, even indirectly.
    NSLog(@"Task base preparation running");
}
@end

@interface UploadTask : Task
@end

@implementation UploadTask
- (void)prepare {
    [super prepare];   // required -- Task's own preparation must still run
    NSLog(@"UploadTask-specific preparation running");
}
@end
@interface Circle : NSObject

- (instancetype)initWithRadius:(CGFloat)radius NS_DESIGNATED_INITIALIZER;

// Plain init is forbidden outright: a Circle constructed without a radius
// would be meaningless, so calling code should never be able to do this.
- (instancetype)init NS_UNAVAILABLE;

@end

@implementation Circle
- (instancetype)initWithRadius:(CGFloat)radius {
    self = [super init];
    if (self) {
        _radius = radius;
    }
    return self;
}
@end

// [[Circle alloc] init];               // compile error -- NS_UNAVAILABLE
Circle *circle = [[Circle alloc] initWithRadius:5.0];   // correct
@interface FocusModeManager : NSObject

- (void)activateFocusMode API_AVAILABLE(ios(13.0));

@end

- (void)tryActivatingFocus:(FocusModeManager *)manager {
    if (@available(iOS 13.0, *)) {
        [manager activateFocusMode];
    } else {
        NSLog(@"Focus mode requires iOS 13 or later");
    }
}
@interface NetworkClient : NSObject

- (void)fetchDataWithCompletion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion
    API_DEPRECATED_WITH_REPLACEMENT("fetchDataAsync", ios(9.0, 13.0));

- (void)fetchDataAsync API_AVAILABLE(ios(13.0));

@end

// Calling the old method now produces a compiler warning naming the
// exact replacement, often with an automatic Xcode fix-it:
// warning: 'fetchDataWithCompletion:' is deprecated: first deprecated in
// iOS 13.0 - use fetchDataAsync

How These Annotations Show Up Together in Real Headers

A mature, professionally maintained Objective-C header rarely uses just one of these annotations in isolation -- a typical class header wraps its entire interface in NS_ASSUME_NONNULL_BEGIN/END, marks its one true designated initializer with NS_DESIGNATED_INITIALIZER while marking the inherited plain init NS_UNAVAILABLE, marks any lifecycle method a subclass must extend (rather than replace) with NS_REQUIRES_SUPER, and marks any method introduced after the framework's original release with API_AVAILABLE, later moving older, superseded methods to API_DEPRECATED_WITH_REPLACEMENT as newer alternatives take over. Reading Apple's own system framework headers with this lesson's vocabulary in mind makes clear just how much of their API design is actually enforced by the compiler rather than left to documentation alone.

@implementation UploadTask - (void)prepare { // Forgot [super prepare]; -- Task's base bookkeeping never runs. NSLog(@"UploadTask-specific preparation running"); } @end

@implementation UploadTask - (void)prepare { [super prepare]; NSLog(@"UploadTask-specific preparation running"); } @end

NS_REQUIRES_SUPER produces a compiler warning when a required super call is missing, but it does not produce a hard compile error the way NS_UNAVAILABLE does -- code that ignores the warning compiles and runs, silently skipping whatever bookkeeping Task's own prepare performs. Because nothing crashes immediately, this bug can go unnoticed until the missing base-class setup causes a failure somewhere else entirely, far from this override.

Inside an NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END region, what happens to a pointer type that has no explicit nullability annotation? A. The compiler treats it as nullable by default, matching Objective-C's historical behavior B. The compiler treats it as nonnull by default -- nullable must be written explicitly as the exception C. The compiler ignores nullability entirely inside such a region D. It depends on whether the pointer is a parameter or a return type

B. The compiler treats it as nonnull by default -- nullable must be written explicitly as the exception — NS_ASSUME_NONNULL_BEGIN flips the default: every unannotated pointer type inside the region is treated as nonnull, and any pointer that genuinely can be nil must be marked nullable explicitly, as the exception rather than the default.

What does NS_REQUIRES_SUPER actually enforce, and how strongly? A. It is a hard compile error if an override omits the super call, exactly like NS_UNAVAILABLE B. It has no compiler-level effect at all -- it is purely documentation for other developers C. It produces a compiler warning when an override does not call the superclass implementation anywhere in its body, but code that ignores the warning still compiles and runs D. It automatically inserts a call to the superclass implementation for you, whether or not the override calls it explicitly

C. It produces a compiler warning when an override does not call the superclass implementation anywhere in its body, but code that ignores the warning still compiles and runs — NS_REQUIRES_SUPER backs its contract with a real compiler warning when a required super call is missing, which is meaningfully stronger than a comment -- but it stops short of being a hard error, so an override that ignores the warning still compiles, silently skipping whatever the superclass's implementation was responsible for.

What is the standard reason to mark a class's inherited plain init as NS_UNAVAILABLE? A. To make the class impossible to allocate under any circumstances B. To force every caller through the class's actual designated initializer, preventing construction of a half-configured object via plain init C. It is purely stylistic and has no effect on what code is allowed to compile D. NS_UNAVAILABLE on init is required by the compiler for every Objective-C class

B. To force every caller through the class's actual designated initializer, preventing construction of a half-configured object via plain init — Marking plain init NS_UNAVAILABLE is a hard compile error for any caller who tries to use it, which is the standard technique for steering every caller toward a designated initializer that actually requires the configuration a correctly constructed object needs.

What is the key difference between API_DEPRECATED and API_UNAVAILABLE? A. They are identical -- API_UNAVAILABLE is simply an older name for API_DEPRECATED B. API_DEPRECATED still allows the symbol to be called, with a compiler warning; API_UNAVAILABLE means the symbol cannot be called on that platform at all, a compile error C. API_DEPRECATED applies only to methods, while API_UNAVAILABLE applies only to whole classes D. API_UNAVAILABLE only affects Swift code, never Objective-C code

B. API_DEPRECATED still allows the symbol to be called, with a compiler warning; API_UNAVAILABLE means the symbol cannot be called on that platform at all, a compile error — A deprecated API still compiles and runs, with a warning telling callers to stop relying on it; an unavailable API produces an outright compile error on the platform it is marked unavailable for, since the symbol is not meant to be called there under any circumstances.

How does the Swift importer handle a method marked NS_UNAVAILABLE in its Objective-C header? A. Swift code can still call it, but receives a runtime warning each time B. The method is not imported into Swift's API surface at all -- Swift code has no way to call it C. NS_UNAVAILABLE has no effect on the Swift importer; only API_UNAVAILABLE does D. The method is imported into Swift but automatically renamed to avoid the conflict

B. The method is not imported into Swift's API surface at all -- Swift code has no way to call it — A method marked NS_UNAVAILABLE is simply absent from what the Swift importer exposes -- as far as Swift code is concerned, the method does not exist at all, which is consistent with NS_UNAVAILABLE making the method an outright compile error to call from Objective-C too.

Write an @interface for a class ConnectionManager with: a designated initializer initWithHost:(NSString *)host that is the only correct way to construct it, with plain init marked unavailable; a method open marked NS_REQUIRES_SUPER, whose base implementation logs that the connection state machine is starting; and a method legacyOpenSynchronously marked as deprecated in iOS 13.0 (having been available since iOS 8.0) in favor of a replacement named openAsync, which is itself marked available only from iOS 13.0 onward. Wrap the whole interface in NS_ASSUME_NONNULL_BEGIN/END.

NS_ASSUME_NONNULL_BEGIN @interface ConnectionManager : NSObject - (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; - (void)open NS_REQUIRES_SUPER; - (void)legacyOpenSynchronously API_DEPRECATED_WITH_REPLACEMENT("openAsync", ios(8.0, 13.0)); - (void)openAsync API_AVAILABLE(ios(13.0)); @end NS_ASSUME_NONNULL_END @implementation ConnectionManager - (instancetype)initWithHost:(NSString *)host { self = [super init]; if (self) { _host = host; } return self; } - (void)open { NSLog(@"Connection state machine starting"); } @end

You will need five annotations total on this one interface: NS_ASSUME_NONNULL_BEGIN/END around the whole block, NS_DESIGNATED_INITIALIZER on initWithHost:, NS_UNAVAILABLE on init, NS_REQUIRES_SUPER on open, and both API_AVAILABLE and API_DEPRECATED_WITH_REPLACEMENT used together as shown in this lesson's examples.