Swift's Objective-C bridging and initializers

I ran into an interesting issue the other day with an Objective-C API that was bridged for use in Swift. For anyone who hasn’t used this functionality before, you can designate one or more Objective-C library header files to include in the workspace by way of a “bridging header”. All of those APIs will be made available to a Swift app. Not only do you get global use of the headers’ APIs, but you get it using Swift’s syntax. It’s pretty neat.

So if you have an Objective-C API:

@interface MyObject: NSObject
- (instancetype)initWithFoo:(FooClass *)aFoo;
- (void)doSomethingWithString:(NSString *)someString;
@end

Swift and the compiler automatically make the following syntax available to you:

let obj = MyObject(foo: aFoo)
obj.doSomethingWithString("bar")

You get both the initializer (which drops the initWith and lowercases the Foo) as well as the instance method, which is translated verbatim.

The problem

The problem came about with a class method. Usually, these get translated from Objective-C:

@interface UIColor: NSObject
+ (instancetype)redColor;
@end

In order to be used as follows in Swift:

let aRed = UIColor.redColor()

But I had a legacy singleton class method from an upstream project:

@interface LegacyDoodad: NSObject
+ (instancetype)doodad;
@end

Besides being slightly bad form for singleton method naming, it doesn’t work in Swift:

let myDoodad = LegacyDoodad.doodad()

The compiler reports:

'doodad' is unavailable: use object construction 'LegacyDoodad()'

Why the problem?

Notice in the first example how the Swift compiler was able to recognize that initWithFoo: in Objective-C was an initializer and parse it appropriately. In Swift, there is also the concept of convenience initializers, initializers marked convenience when declared directly in Swift. These initializers generally take no arguments, providing their own sane defaults, as a way to “shortcut” longer, more verbose designated initializers. It turns out that the Swift compiler thinks that doodad is a convenience initializer because:

  1. It takes no arguments.
  2. It is named in a way that is derivative of the class name.

Thus, it doesn’t translate doodad and just defers to the automatically-provided default initializer (roughly analagous to -[[LegacyDoodad alloc] init]) provided to every class, which is called like LegacyDoodad() — also with no arguments.

The fix

The fix turns out to be pretty simple as long as you take a look at how Apple designs APIs. A good model is UIApplication, which is always accessed via a singleton:

@interface UIApplication: NSObject
+ (instancetype)sharedApplication;
@end

This is used as you’d expect in Swift:

let app = UIApplication.sharedApplication()

Using a singleton naming scheme like sharedFoo (e.g., UIApplication), defaultFoo (e.g., NSFileManager), or sharedInstance (commonly seen in third-party libraries) ensures that the Swift compiler doesn’t get fooled into thinking it’s a convenience initializer.

Here’s a way to amend the original example to work the way we’d expect:

@interface LegacyDoodad: NSObject
+ (instancetype)sharedDoodad;
@end

And in Swift:

let myDoodad = LegacyDoodad.sharedDoodad()

Further reading

Special thanks to the following StackOverflow posts which helped me get to the bottom of this:

The original reported issue in our project queue:

A nice blog post about singleton method naming in Objective-C:

And Apple’s reference documentation on Swift initializers:


Changes: