Lightweight Generics and Nullability

This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Lightweight generics and nullability are annotations: they add compile-time information without changing what happens at run time. Clang introduced both to improve Objective-C’s own diagnostics and — decisively — so that Objective-C APIs import into Swift as properly typed, properly optional declarations rather than as seas of AnyObject!.

Lightweight Generics

Parameterised Collections

NSArray<NSString *> *names = @[ @"Ada", @"Alan" ];
NSDictionary<NSString *, NSNumber *> *ages = @{ @"Ada": @36 };
NSSet<Item *> *items = [NSSet set];
NSMutableArray<NSURL *> *urls = [NSMutableArray array];

NSString *first = names[0];             // typed: no cast needed
// [names addObject:@42];               // warning: incompatible pointer types

The payoff is at the call site: elements come out correctly typed, and putting the wrong thing in produces a warning.

Nested parameterisation works as expected:

NSDictionary<NSString *, NSArray<Item *> *> *itemsBySection;
NSArray<NSDictionary<NSString *, id> *> *jsonObjects;

These are erased at run time. The type argument exists only for the compiler; NSArray<NSString *> and NSArray<NSNumber *> are the same class, and nothing stops an untyped code path from inserting the wrong element. Treat generics as strong documentation plus a good warning, not as a guarantee — validate data arriving from JSON or from disk regardless.

Annotating a collection type is worth doing everywhere: in properties, in method signatures, and in local variables where it aids readability.

Generic Classes

Your own classes can be parameterised too:

@interface MyStack<ObjectType> : NSObject

- (void)pushObject:(ObjectType)object;
- (nullable ObjectType)popObject;
@property (nonatomic, readonly) NSArray<ObjectType> *allObjects;

@end

ObjectType is a placeholder usable anywhere a type is expected in the interface. The implementation simply ignores it — there is no specialisation, so inside @implementation the parameter behaves as id:

@implementation MyStack {
    NSMutableArray *_storage;          // plain: no type parameter needed here
}

- (void)pushObject:(id)object {        // ObjectType erases to id
    [_storage addObject:object];
}

@end

Usage is what you would expect:

MyStack<NSString *> *stack = [[MyStack alloc] init];
[stack pushObject:@"first"];
NSString *top = [stack popObject];      // typed
// [stack pushObject:@42];              // warning

You may also constrain the parameter and supply a default:

@interface MyBox<ObjectType : id<NSCopying>> : NSObject     // must be copyable
@end

@interface MyList<__covariant ObjectType> : NSObject
@end

covariant and contravariant

Variance says how a parameterised type behaves under subtyping:

Qualifier Meaning

__covariant

MyList<Circle > * is usable where MyList<Shape *> * is expected — correct for a *producer (something you read values out of). This is what Foundation’s collections use.

__contravariant

MyHandler<Shape > * is usable where MyHandler<Circle *> * is expected — correct for a *consumer (something you pass values into).

(neither)

Invariant: only an exact match is accepted.

@interface MyProducer<__covariant ObjectType> : NSObject
- (ObjectType)next;
@end

MyProducer<Circle *> *circles = …;
MyProducer<Shape *>  *shapes  = circles;     // allowed: __covariant

@interface MyConsumer<__contravariant ObjectType> : NSObject
- (void)consume:(ObjectType)object;
@end

MyConsumer<Shape *>  *anyShape = …;
MyConsumer<Circle *> *circleC  = anyShape;   // allowed: __contravariant

Rule of thumb: covariant if the parameter appears only in return positions, contravariant if only in argument positions, and neither if it appears in both.

__kindof

__kindof Type * means "this type or any subclass", and exists to remove the downcast that otherwise clutters factory- and container-style APIs:

// Without __kindof: the caller writes (MyButton *)[view viewWithTag:7]
- (__kindof UIView *)viewWithTag:(NSInteger)tag;

MyButton *button = [container viewWithTag:7];        // no cast
[button setTitle:@"OK"];                             // MyButton's own methods are fine

It also combines with generics — NSArray<__kindof UIView *> * is an array whose elements are `UIView`s or any subclass, assignable to a more specific variable without a cast. As with everything else on this page, it is checked at compile time only.

Nullability

The Audited-Region Idiom

Annotating every pointer individually would be unbearable, so Clang provides a region macro. Inside it, every unannotated object pointer is assumed nonnull, and you annotate only the exceptions:

NS_ASSUME_NONNULL_BEGIN

@interface Downloader : NSObject

@property (nonatomic, copy)             NSURL    *url;          // implicitly nonnull
@property (nonatomic, copy, nullable)   NSString *authToken;    // explicitly nullable

- (nullable NSData *)cachedDataForURL:(NSURL *)url;
- (void)fetchURL:(NSURL *)url
      completion:(void (^)(NSData * _Nullable data, NSError * _Nullable error))completion;

@end

NS_ASSUME_NONNULL_END

Wrap every header in NS_ASSUME_NONNULL_BEGIN/END. It is the house convention across Apple’s own SDKs, and a header that is entirely unaudited imports into Swift as implicitly-unwrapped optionals everywhere, which is the worst of both worlds.

The Four Qualifiers

Property-attribute form Type form Meaning

nonnull

_Nonnull

Never nil. The default inside an audited region.

nullable

_Nullable

May be nil; callers must handle that.

null_resettable

(n/a)

The getter never returns nil, but the setter accepts nil to reset to a default. Rare; UIView’s `tintColor is the classic example.

null_unspecified

_Null_unspecified

Deliberately unaudited. Imports into Swift as an implicitly-unwrapped optional.

The two spellings differ only in position. The nullable form goes in a property’s attribute list or before a method’s return type; the _Nullable form goes after the *, and is what you must use in C-style contexts — inside a block signature, on a pointer-to-pointer, or in a function declaration:

@property (nonatomic, strong, nullable) NSString *name;
- (nullable NSString *)nameForKey:(NSString *)key;

// In these positions only _Nullable/_Nonnull work:
- (BOOL)loadAndReturnError:(NSError * _Nullable * _Nullable)error;
typedef void (^Handler)(NSData * _Nullable data, NSError * _Nullable error);
extern NSString * _Nullable MyLookup(NSString * _Nonnull key);

That NSError * spelling deserves attention: the *pointer may be NULL (the caller does not want an error), and the NSError * it points to may be nil. Both levels need annotating. See Errors and Exceptions.

Nullability is a compile-time warning, not a run-time guarantee. Passing nil to a nonnull parameter produces a warning, but if it happens anyway — via id, from a dynamic call, or from Swift’s force-unwrapping — nothing stops it. Keep validating input at trust boundaries.

@available and Availability Attributes

Nullability describes what a value may be; availability describes when an API exists.

@available: the Run-Time Check

if (@available(iOS 15.0, macOS 12.0, *)) {
    [self useModernAPI];                 // only on new-enough systems
} else {
    [self useFallback];
}

The trailing means "on any other platform, assume it is available" and is required. Clang understands @available well enough to *silence the "only available on iOS 15.0 or newer" warning inside the branch — which a hand-written version check such as [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:] cannot do. Always prefer @available.

Note the distinction from #if TARGET_OS_IPHONE: that is a compile-time question about which platform you are building for. @available is a run-time question about which OS the binary is executing on.

Marking Your Own API

@interface MyService : NSObject

- (void)modernMethod API_AVAILABLE(ios(15.0), macos(12.0));

- (void)oldMethod
    API_DEPRECATED("Use modernMethod instead", ios(9.0, 15.0), macos(10.11, 12.0));

- (void)goneMethod API_UNAVAILABLE(tvos, watchos);

- (instancetype)init NS_UNAVAILABLE;              // cannot be called at all

@property (nonatomic, readonly) NSInteger legacyCount
    __attribute__((deprecated("Use count instead")));

@end

API_DEPRECATED takes the version the API appeared and the version it was deprecated in, plus a message naming the replacement — which is what makes the resulting compiler warning actionable.

What This Means on the Swift Side

These annotations exist largely for Swift’s benefit, and the mapping is direct:

Objective-C Imports into Swift as

NSString * (unaudited)

String! — implicitly unwrapped; unsafe

nonnull NSString *

String — non-optional

nullable NSString *

String? — optional

null_resettable NSString *

String!

NSArray<NSString *> *

[String]

NSDictionary<NSString *, NSNumber *> *

[String: NSNumber]

NSArray * (unparameterised)

[Any]

__kindof UIView *

UIView

NS_ENUM

enum

NS_OPTIONS

OptionSet

- (BOOL)doThing:(NSError **)error

func doThing() throws

instancetype

Self — correct in subclasses

API_AVAILABLE(ios(15.0))

@available(iOS 15.0, *)

The practical consequence: an Objective-C header that is audited for nullability and parameterised with generics produces a Swift API indistinguishable from a native one, while an unaudited header produces something awkward and unsafe. If your code will be consumed from Swift at all, both annotations are effectively mandatory. See Swift Interoperability.

See Also