Objective-C's Designated Secret

For the past several iterations of Xcode, Apple has quietly but steadily improved the quality of the backing toolchain. In particular, the clang static analyzer has gotten quite a few improvements; however, LLVM hasn’t been neglected.

Far from it. In Xcode 5.1, Apple shipped an updated version of LLVM that included upstream revision 196314. The summary of that change is brief – “Introduce attribute objc_designated_initializer” – but the potential implications are wide-ranging. But first: what are they even talking about?

Objective-C Designated Initializers

In many object-oriented languages, there’s the concept of the designated initializer: the one constructor or setup method that all others call. Few languages, however, encourage use of this pattern quite as much as Objective-C and the associated frameworks do.

Designated initializers provide a great convenience for subclassers. Consider, for example, a hypothetical (and stereotypical) class BankAccount that tracks a person’s balance. You could imagine how such a class would have two initializers:

@interface BankAccount : NSObject
@property (copy) NSString *name;
@property (copy) NSDecimalNumber *balance;
- (id)initWithName:(NSString *)name;
- (id)initWithName:(NSString *)name balance:(NSDecimalNumber *)balance;
@end

A naive implementation would set both of these up separately:

@implementation BankAccount

- (id)initWithName:(NSString *)name {
    if ((self = [super init])) {
        self.name = name;
        self.balance = [NSDecimalNumber zero];
    }
    return self;
}

- (id)initWithName:(NSString *)name balance:(NSDecimalNumber *)balance {
    if ((self = [super init])) {
        self.name = name;
        self.balance = balance;
    }
    return self;
}

@end

On the surface, this all looks OK. But in the case that someone attempted to subclass BankAccount – or even to make a change to one of the init methods – they might find themselves writing duplicate code, or even introducing a subtle bug. Let’s take the former approach and try to make a subclass for jointly held accounts:

@interface JointBankAccount : BankAccount
@property (strong) NSMutableArray *coOwners;
@end

Now, since each initializer sets up all the account’s properties separately, our subclass is required to do the same:

@implementation JointBankAccount

- (id)initWithName:(NSString *)name {
    if ((self = [super initWithName:name])) {
        self.coOwners = [NSMutableArray array];
    }
    return self;
}

- (id)initWithName:(NSString *)name balance:(NSDecimalNumber *)balance {
    if ((self = [super initWithName:name balance:balance])) {
        self.coOwners = [NSMutableArray array];
    }
    return self;
}

@end

See the trouble? Both initializers do the exact same setup – future changes have to be made in two places, and it’s a pain to write this boilerplate to begin with. We could refactor out a common subclass init helper, but for a single extra line of setup that’s hard to justify. Here’s the kicker: all of this could be cleaned up significantly with a designated initializer.

Making Use of Designated Initializers

The grand idea behind designated initializers is a deceptively simple one: all of your class’s init methods should eventually call through to the same final method. That method is your designated initializer.

For our BankAccount class above, we can designate the more verbose initializer and rewrite as:

@implementation BankAccount

- (id)initWithName:(NSString *)name {
    return [self initWithName:name balance:[NSDecimalNumber zero]];
}

- (id)initWithName:(NSString *)name balance:(NSDecimalNumber *)balance {
    if ((self = [super init])) {
        self.name = name;
        self.balance = balance;
    }
    return self;
}

@end

Notice how the former initializer no longer calls super, nor does it set up any properties on self. Instead, it simply picks a default value for a new account’s balance, then passes that argument – along with the provided name – through to the more verbose initializer. That latter initializer, as the designated initializer, is responsible for fully configuring the new instance.

This immediately gives us some benefits in our JointBankAccount subclass. Instead of overriding both initializers to do the same work, we can be assured that the designated initializer will eventually run no matter which initializer is originally called. That lets us only subclass the designated initializer. No more duplicated code!

Do What I Say

There’s just one problem left: how do we communicate which initializer is the designated one to a user of the class? Most frequently, this is accomplished with documentation; however, such things can get out of date, be inaccurate, or just plain get ignored by the developer writing code against our class. This makes it easy for future development to introduce other subtle bugs.

For example, let’s add one more initializer to our base BankAccount class. This time, let’s add a balance-only initializer. (Pretend we’re writing software for a Swiss bank: you can have an anonymous account if you so desire.)

@interface BankAccount : NSObject
// ... other properties and initializers ...
- (id)initWithBalance:(NSDecimalNumber *)balance;
@end

@implementation BankAccount
// ... other initializer implementations ...

- (id)initWithBalance:(NSDecimalNumber *)balance {
    if ((self = [super init])) {
        self.name = NSLocalizedString(@"Anonymous", nil);
        self.balance = balance;
    }
    return self;
}

@end

Suddenly we’ve written a bug! This code will compile cleanly and run just fine… until your subclasser doesn’t get a chance to run their custom init code, since this initializer doesn’t call the designated initializer.

If we remembered, we could pretty easily correct this initializer’s implementation: just pass the name “Anonymous” and the given balance through to the designated initializer. But that’s one more thing to remember (or read) about this class, and in a larger set of changes, this kind of detail could easily slip past even an experienced developer.

The NS_DESIGNATED_INITIALIZER Attribute

This is where the compiler can help us out. The major benefit of r196314 is the addition of a method attribute notifying LLVM that a particular initializer is considered designated, and that other initializers on the class must call through to it.

Note: Although the raw compiler attribute is available as early as Xcode 5.1, Apple did not expose public macros for it until Xcode 6. The following code uses Apple’s name; if compatibility with earlier versions is required, use LLVM’s __attribute__((objc_designated_initializer)) instead.

To add the designated initializer attribute to a method, append the following incantation to the declaration in the header (after the last attribute, but before the semicolon):

NS_DESIGNATED_INITIALIZER

Applying this to our BankAccount class, we wind up with:

@interface BankAccount : NSObject
@property (copy) NSString *name;
@property (copy) NSDecimalNumber *balance;
- (id)initWithName:(NSString *)name;
- (id)initWithBalance:(NSDecimalNumber *)balance;
- (id)initWithName:(NSString *)name balance:(NSDecimalNumber *)balance
        NS_DESIGNATED_INITIALIZER;
@end

At this point, attempting to compile this class – with the three initializer implementations we’ve defined earlier – will throw a compiler warning in the -initWithBalance: method, since it’s an initializer that is neither designated nor calls through to the designated initializer. More specifically, if we compile this BankAccount implementation, we see:

BankAccount-v1.m:26:24: warning: secondary initializer should not invoke an initializer on 'super' [-Wobjc-designated-initializers]
    if ((self = [super init])) {
                       ^
BankAccount-v1.m:25:1: warning: secondary initializer missing a 'self' call to another initializer [-Wobjc-designated-initializers]
- (id)initWithBalance:(NSDecimalNumber *)balance {
^
2 warnings generated.

(Again, this would be relatively easy to correct; a revised implementation silences these warnings by forwarding the secondary -initWithBalance: initializer to the more verbose designated initializer.)

Causing More Trouble

There are several other ways that we can upset the compiler once we’ve started using NS_DESIGNATED_INITIALIZER. You can cause a warning by doing any of the following.

Designate an initializer, but forward it to a secondary initializer. We might do this on our BankAccount by trying to use -initWithName: to set self.name, then calling that initializer from -initWithName:balance:. Since the latter is our designated initializer, it’s not allowed to call across to any secondary initializers. (See this implementation.)

Subclass and call a non-designated initializer on the superclass. Borrowing our JointBankAccount subclass idea from earlier: we might be tempted to call -initWithBalance:, then manage the list of owners separately instead of using the designated initializer (which takes a name as well). This causes its own trouble, since calling the superclass’s initializers has to call through to a designated initializer. (See this example.)

Subclass and designate a new initializer. Continuing with the JointBankAccount: what if we wanted a brand new initializer that took co-owners as a parameter? You can get away with this one, but only if you also override the old designated initializer. (See this warning-free sample.)

Though these cases may seem daunting at first, you’ll soon fall into a pattern of forwarding to designated initializers everywhere you need to. In the more complex situations, Apple provides the definitive rules for designated initializers over at their Object Initialization page; you can refer to them as you need to.

Looking to Swift

Up to this point, we’ve discussed NS_DESIGNATED_INITIALIZER entirely in Objective-C, and Apple certainly introduced the attribute first in that language. But Swift is the way forward, and so it’s only fitting that we close on Swift’s implementation of designated initializers.

In Swift, designated status has been promoted from a compiler attribute to a few language keywords, and the sense of the thing has flipped around. Now:

  • Any custom initializer you define (using the init keyword) is assumed to be a designated initializer
  • You can mark your initializers as not designated using the convenience keyword

With these rules, reworking our BankAccount in Swift might look something like this:

class BankAccount {
    var name: String
    var balance: Int

    convenience init(name: String) {
        self.init(name: name, balance: 0)
    }
    
    convenience init(balance: Int) {
        self.init(name: NSLocalizedString("Anonymous", nil), balance: balance)
    }
    
    init(name: String, balance: Int) {
        self.name = name
        self.balance = balance
    }
}

Notice here how the designated initializer doesn’t have any keyword marking it as such; instead, all the non-designated initializers are explicitly marked “convenience.” (The other rules about subclassing and forwarding init calls all still apply, though!)

Wrapping Up

Though it’s been quite some time since designated initializers were made available in Objective-C, they’re gaining traction quickly thanks to Swift and its implicit designation behavior. By starting to mark designated initializers in Objective-C and convenience initializers in Swift, you can more clearly communicate the intent of your code and rely on some compiler help to reduce bugs in your apps.

(Got comments? Have a particularly interesting application of designated initializers? Am I wrong on the Internet? Find me on Twitter!)

Update: @peternlewis pointed out an accidental bug in a code sample (as opposed to all the deliberate ones). A couple others also mentioned the use of instancetype vs. id for initializers. Though the former is perhaps more modern, the latter works perfectly well due to some related type inference on the part of LLVM – any method in the init family returning id is inferred to work as if it returned instancetype. Thanks for all the feedback!