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:
- It takes no arguments.
- 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:
- How to access an Objective-C class method from Swift language
- How to call an Objective-C singleton from Swift?
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:
- Jan 28, 2015: Added a link to a second StackOverflow post; added some info about default initializers when talking about why things weren't working.
- Jan 29, 2015: Fixed Objective-C class declaration syntax, which was a weird Objective-C/Swift hybrid.