I recently read a great post by Benjamin Encz about bridging Swift to Objective-C by creating bridging types that can be exposed to the older language. I wondered if his idea can be improved and automated using meta-programming with Sourcery by Krzystof Zablocki. The answer is a very impressive ObjC-like uppercase YES.
First of all, you should read Benjamin’s post. He proposes to use a bridging class that can be instantiated from Objective-C and Swift because it inherits from NSObject
. The bridging class has properties to get and set the properties of the bridged type.
In this post I’m just going to give an overview of the template and highlight the simpler parts. If you want to jump directly in the code, here’s the project, and the template. The example template bridges struct and enum, but doesn’t handle custom protocols to keep it as simple as possible.
About Sourcery
Sourcery is a tool to generate code, using your written templates, using metadata from your production code. In other words, it’s a tool to do metaprogramming in Swift. If you are not familiar with Sourcery I encourage you to go read the documentation and at least understand the simple examples like generating Equatable
implementations for your types. Getting familiar with Stencil, one of the supported template languages is also fundamental.
Building the template
We’ll need to generate a new class for every struct and enum that we bridge. In order to opt-in to the feature, we use phantom types in order to mark swift types (our custom structs or enums) that can be bridged to Objective-C. Our template will use this protocol to which types to introspect:
1
|
|
For example, take Benjamin’s example code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
We just need to adopt the protocol in an extension for the types, in order to opt-in to code generation with Sourcery:
1 2 |
|
Structs vs enums
Structs are straightforward to bridge, as we only need to handle properties. On the other hand, enums are much more involved because we need to take care of handling an arbitrary number of associated values. Fear not, as Sourcery provides all the necessary metadata for our nefarious needs!
We will divide the template in two parts: one loop for structs, one for enums.
1 2 3 4 5 6 7 |
|
For structs, we only generate one class that contains the Swift native type. We wrap the type with API to set and get properties, following Benjamin’s design. Enumerations will require more classes (see below).
Also bear in mind that when bridging properties in either an enum or a struct, we need to handle differently types that are bridged by ourselves (our custom struct and enums).
Bridging an empty struct
This is how we start generating classes for every bridgeable struct:
1 2 3 4 5 6 |
|
Given a simple, empty struct:
1 2 |
|
Let’s look at what a the resulting bridging code for the simplest struct would look like; An empty struct should only wrap the type in an Objective-C class, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Now for a more useful type, say a struct with some values, like this one:
1 2 3 4 |
|
The bridging code looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
The generated code will need to provide read-only properties for the inmutable properties, and read-write properties for the mutable ones. This example only contains simple types, but not other structs or enumerations. Generating those will require to nest bridged classes. You can check the template code by yourself for the rest of it.
Generating properties for standard Swift types is simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Generating the swift initializer and the wrapped property is also simple:
1 2 3 4 5 6 |
|
Generating the initializer from Objective-C will require to handle structs and enumerations specially, so you can check it yourself in the template.
Bridging simple enumerations
The template code to bridge enumerations is a bit more complex. We’re just going to review what the generated code would look like in this case. Take a simple example:
1 2 3 4 |
|
The generated bridging code should contain a class for the enum, and separate classes for every case. We initialize from Objective-C casting from Any
to the concrete class corresponding to every case. Take note we’ll need to handle associated values and because of this the template code is a bit messy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Conclusion
Automating the generation of complex things like bridging is possible with Sourcery. The template I’ve written doesn’t handle all cases of possible code; For example, the template doesn’t handle private properties, doesn’t take default values for initializers, we don’t forward methods, and it doesn’t handle properly structs as associated values of enums. The template also doesn’t bridge protocols.
I stopped here on purpose, as this generated code should be temporary until a migration eventually happens. In general, interaction from Objective-C to Swift should be kept to a minimum.
I’m very surprised that such complexity can be automated using Sourcery, and I really love the concept of the tool. If you never used it, it’s a very useful addition for your Swift toolbelt.
If you have any feedback, please open an issue in the example repository, or reach me on twitter. I’d love to hear your thoughts!