Using Objective-C Lightweight Generics

I wrote about using nullable to annotate your Objective-C code some time ago. It is some extra work and not pretty but has some advantages if you want to use Objective-C code from Swift. The nullable hints give you a safer Swift interface that does not assume everything to be an ugly and potentially unsafe implicitly unwrapped optional.

What I did not mention is an extra step you can take to improve the Swift interface that also has benefits for the Objective-C code. This post summarises what you need to know about Objective-C Lightweight Generics.

17-Dec-2019 Updated for Swift 5

Foundation Collection Classes

The Xcode 7.0 release notes introduced lightweight generics to the Foundation collection classes:

Lightweight generics now allow you to specify type information for collection classes such as NSArray, NSSet, and NSDictionary. The type information improves Swift access when you bridge from Objective-C, and simplifies the code you have to write.

To understand how this is useful consider an Objective-C class that has three properties as follows:

NS_ASSUME_NONNULL_BEGIN
@property (nonatomic,strong) NSArray *dueDates;
@property (nonatomic,strong) NSDictionary *dataDictionary;
@property (nonatomic,strong) NSSet *filter;
NS_ASSUME_NONNULL_END

The generated Swift (5) interface for these properties is not ideal:

open var dueDates: [Any]
open var dataDictionary: [AnyHashable : Any]
open var filter: Set<AnyHashable>

We may intend for the array of dates to contain NSDate objects but that is not clear to the compiler. This leaves our Swift code looking less than type safe.

We can make our NSArray, NSDictionary and NSSet collections safer by declaring the types of the parameters:

@property (nonatomic,strong) NSArray<NSDate *> *dueDates;
@property (nonatomic,strong) NSDictionary<NSNumber *, NSString *> *dataDictionary;
@property (nonatomic,strong) NSSet<NSString *> *filter;

Compare the difference in the generated Swift interface we get now. We have replaced the Any and AnyHashable types with the actual types we expect in each collection:

open var dueDates: [Date]
open var dataDictionary: [NSNumber : String]
open var filter: Set<String>

Note: This only works for NSArray, NSDictionary and NSSet. Swift ignores lightweight generics for other classes when imported. For example:

// Objective-C
@property (nonatomic,strong) NSMutableArray<NSDate *> *dates;
// Swift
open var dates: NSMutableArray

Good for Swift, Good for Objective-C

Even if you don’t care about Swift interoperability using lightweight generics can help your Objective-C code. The compiler can now warn you when you mistake the contents of a collection:

// NSArray<NSDate *> *dueDates;
NSString *lastDue = self.dueDates.lastObject;

Incompatible pointer types initializing 'NSString *' with an
expression of type 'NSDate * _Nullable'

An example adding an NSString to a mutable array of NSDate objects:

NSMutableArray<NSDate *> *dates = [NSMutableArray new];
[dates addObject:@"today"];

Incompatible pointer types sending 'NSString *' to parameter of
type 'NSDate * _Nonnull'

Adding lightweight generics to your own classes is also possible (see here) - just remember Swift will ignore them when imported.

Further Reading