BS
BleepingSwift
Published on
5 min read

> Lightweight Generics in Objective-C

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

Before Xcode 7, Objective-C collections were completely untyped. An NSArray could contain strings, numbers, custom objects, or a mix of everything. The compiler had no idea what was inside, so you had to cast constantly and hope for the best. Lightweight generics changed that by letting you declare what a collection contains.

Basic Syntax

The syntax mirrors other generic languages. You specify the element type in angle brackets:

NSArray<NSString *> *names = @[@"Alice", @"Bob", @"Charlie"];
NSDictionary<NSString *, NSNumber *> *scores = @{@"Alice": @95, @"Bob": @87};
NSSet<NSURL *> *bookmarks = [NSSet setWithObjects:url1, url2, nil];

Now the compiler knows the types. If you try to add the wrong type, you get a warning:

NSMutableArray<NSString *> *names = [NSMutableArray array];
[names addObject:@"Valid"];
[names addObject:@42];  // Warning: Incompatible pointer types

When you retrieve elements, you get the proper type without casting:

NSString *first = names.firstObject;  // No cast needed
NSUInteger length = first.length;     // Compiler knows this is NSString

Using Generics in Your Own Classes

You can define generic parameters on your own classes. Declare them after the class name:

// Stack.h
@interface Stack<ObjectType> : NSObject

- (void)push:(ObjectType)object;
- (ObjectType)pop;
- (ObjectType)peek;
@property (nonatomic, readonly) NSUInteger count;

@end

The ObjectType placeholder works throughout the interface. In the implementation, you use id since Objective-C generics are erased at runtime:

// Stack.m
@interface Stack ()
@property (nonatomic, strong) NSMutableArray *storage;
@end

@implementation Stack

- (instancetype)init {
    self = [super init];
    if (self) {
        _storage = [NSMutableArray array];
    }
    return self;
}

- (void)push:(id)object {
    [self.storage addObject:object];
}

- (id)pop {
    id object = self.storage.lastObject;
    [self.storage removeLastObject];
    return object;
}

- (id)peek {
    return self.storage.lastObject;
}

- (NSUInteger)count {
    return self.storage.count;
}

@end

Callers get full type checking:

Stack<NSString *> *stringStack = [[Stack alloc] init];
[stringStack push:@"Hello"];
[stringStack push:@123];  // Warning: Incompatible pointer types

NSString *top = [stringStack pop];  // Type is NSString *, no cast needed

Covariance and Contravariance

By default, generic types are invariant. A Stack<NSString *> is not assignable to a Stack<NSObject *> even though NSString is a subclass of NSObject. You can change this with __covariant or __contravariant:

// Covariant: Can assign Stack<Subclass> to Stack<Superclass>
@interface Stack<__covariant ObjectType> : NSObject

With __covariant, this compiles:

Stack<NSString *> *stringStack = [[Stack alloc] init];
Stack<NSObject *> *objectStack = stringStack;  // OK with __covariant

This matches how NSArray works in the standard library. Apple declares it as NSArray<__covariant ObjectType> so you can pass an NSArray<NSString *> where NSArray<id> is expected.

Use __contravariant for the opposite direction, which is useful for types that consume rather than produce values:

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

With contravariance, a Consumer<NSObject *> can be assigned to Consumer<NSString *> because a consumer that handles any object can certainly handle strings.

The Compile-Time Limitation

Lightweight generics exist purely at compile time. At runtime, the type information is gone. This means you cannot:

Check the generic type at runtime:

// This doesn't work - there's no runtime generic type info
if ([array isKindOfClass:[NSArray<NSString *> class]]) {  // Meaningless

Use generics with performSelector or other runtime features:

// Runtime doesn't know about generics
NSArray *result = [object performSelector:@selector(names)];
// Compiler doesn't know result is NSArray<NSString *>

This is different from Swift generics, which retain type information at runtime. Objective-C chose compile-time-only generics for backward compatibility with existing code and the runtime.

Nullability and Generics Together

Lightweight generics pair well with nullability annotations. Together they provide a complete type description:

@interface UserManager : NSObject

@property (nonatomic, copy, readonly) NSArray<User *> *allUsers;
- (nullable User *)userWithID:(NSString *)identifier;
- (NSArray<User *> *)usersMatchingPredicate:(NSPredicate *)predicate;

@end

This tells both the compiler and Swift exactly what to expect. The combination of generics and nullability makes Objective-C APIs bridge to Swift much more naturally.

Swift Interop Benefits

The real payoff comes when Swift imports your Objective-C code. Without generics:

// Objective-C
@property (nonatomic, copy) NSArray *names;

Swift sees this as [Any], requiring constant casting:

// Swift
let firstName = manager.names.first as? String

With generics:

// Objective-C
@property (nonatomic, copy) NSArray<NSString *> *names;

Swift sees the proper type:

// Swift
let firstName = manager.names.first  // Already String?

This removes friction in mixed codebases. If you maintain Objective-C code that Swift consumes, adding generics to your collection properties and return types is one of the highest-value improvements you can make.

Practical Adoption

For existing codebases, start with the most-used interfaces. Properties that return collections are good candidates:

// Before
@property (nonatomic, copy) NSArray *items;
@property (nonatomic, strong) NSDictionary *cache;

// After
@property (nonatomic, copy) NSArray<Item *> *items;
@property (nonatomic, strong) NSDictionary<NSString *, NSData *> *cache;

Method parameters and return types benefit too:

// Before
- (NSArray *)fetchResults;
- (void)processItems:(NSArray *)items;

// After
- (NSArray<Result *> *)fetchResults;
- (void)processItems:(NSArray<Item *> *)items;

The changes are purely additive. Existing code continues to work because generics are erased at runtime. You can adopt them incrementally without breaking anything.

Lightweight generics do not give you the full power of Swift or C++ generics, but they eliminate a whole category of type mismatch bugs and make your Objective-C code play nicely with Swift. For the small effort of adding angle brackets, you get compile-time safety and better tooling support.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.