TIL: Forward Declaration, Time Travel, and You
TL; DR: If you have an existing class in Objective-C that has a property whose type is defined by a forward declaration (the @class keyword) and you subclass that Obj-C class in Swift, you can’t use the superclass’ property unless you include the property’s class’ header in your project’s bridging header.
It happens to the best of us: here you are in the Year of our Lord Two Thousand and Sixteen, writing beautiful, protocol-oriented Swift, when you turn a corner and are confronted with some ancient piece of Objective-C code. We all have to deal with dated code now and again, and a rewrite isn’t always possible. Thankfully, Swift has been thoughtfully designed to work (relatively) well with its older sibling, Objective-C. There are some gotchas, though, and I ran into one earlier today involving forward declaration.
What is forward declaration?
In Objective-C, classes have both an interface (“here’s how you interact with this class”), and an implementation (“here’s how the class actually works”). The interface is defined in the class’ header file, and the implementation is defined in a file of the same name, but with the .m extension instead.
Sometimes, you’ll want to refer to another class in a header file for the purposes of adding a property to your interface whose type is defined elsewhere. You could use `#import “OtherClass.h”` in your header, but since all the header really needs to know are the types to expect, you can instead tell the compiler that OtherClass will be defined later using `@class OtherClass` instead. In other words, the class of this property will be defined in the future.
Take a look at the following header and implementation files for my car, the inimitable 1981 DeLorean DMC-12:
As you can see above, the DeLorean is an object, and it has two properties: a readonly pointer to an object of type `FluxCapacitor`, and a read/write integer called `speed`. The FluxCapacitor class exists somewhere, but the header doesn’t need to know about its details — it just needs to know the name of the type of the `fluxCapacitor` property.
When do we need to know specifics about the FluxCapacitor’s interface? In the DeLorean’s implementation:
Here we’re actually importing the FluxCapacitor’s interface, as well as instantiating one in the DeLorean’s designated initializer method. You can also see that when the speed reaches 88 miles per hour, we’re likely to see some serious shit.
Pretty straightforward, but hey, this is 2016 — we can do better.
Subclassing with Swift
I want the behaviour of my trusty DMC-12, but I want it in a more attractive and energy-efficient form factor. Thankfully, Sir Musksalot has my back. Enter the DMC-12 Tesla conversion:
We’ve subclassed the original DeLorean, but we’ve got some fancier stuff under the hood. Thanks to the combined powers of Swift and 21st century battery technology, our new car can reach critical velocity in half the time. We achieve this by overriding the superclass’ property implementation and substituting our own.
You would very reasonably expect that we would have access to our superclass’ properties in this context, such that we can call `fluxCapacitor.activate()` in the subclass and have everything work as desired. Unfortunately, you would be wrong. In fact, this won’t even compile:
What? “Use of unresolved identifier”? How can that be? It’s defined right there in the interface of our superclass!
The reason why this doesn’t work is hiding in the one file we haven’t talked about yet.
The Bridging Header
Somewhere in our project, there is a file called “<ProjectName>-Bridging-Header.h”. In reality, this file can be called whatever you want, as long as you specify its location correctly in the project’s Info.plist file.
Currently, the file looks something like this:
Not very exciting, but the key to our problem is not what’s here, but rather what isn’t.
Because our superclass’ public interface does not import the definition of the property we’re using, the Swift compiler can’t find the details of the class. Apparently, rather than going to find the appropriate header in the project, the Swift compiler appears to just erase the whole property from the superclass, and we’re left with a somewhat cryptic error about it never having been defined in the first place.
Thankfully, the solution is simple: tell Swift about the details of the forward-declared type by including its header in the project’s bridging header:
Now Swift can figure out what a FluxCapacitor is, what public methods and properties it has, etc.
Xcode is happy once again, and we can hop in our newly-souped-up time machine and speed off to relive the glory days of the early 1980s in style. Or, we can go back to binge watching Stranger Things on Netflix for the fifth time. You know, whatever.