Apple’s new programming language has been around for 3 years now and it’s adopted really quickly by iOS developers. However, Objective-C is still around and used a lot. There are a lot of legacy projects that need to be maintained and also a lot of big complex frameworks written in Objective-C, for which it doesn’t make sense to be written from scratch in Swift.
Having this in mind, Apple has already provided good interoperability, which enables developers to reuse what they have developed in Objective-C.
However, sometimes we want to use some really cool Swift feature instead of the one that the Objective-C framework provides, but only in Swift projects. We want to be able to keep the same code running in Objective-C, so we don’t break the existing projects. Basically, we want to inject some code to the framework from the outside, without changing it.
One example for this might be logging. Let’s say we have some bigger framework, which among its features, also provides a wrapper for logging – which displays the log level, the class that invoked it, the time and so on. In our Objective-C framework, this might be implemented as C funtion, since logging is a common operation and it needs to be fast:
But now that Swift arrived, we want to replace it with new logging framework, let’s say CleanroomLogger, when we use it in Swift projects. Also, we have additional constraints – our framework is completely Objective-C, it’s not dynamic framework and we can’t introduce the new logger in it, because we will need to do a lot of changes in the existing projects – converting it to dynamic framework, updating the projects and so on.
How can we do such thing? We can use Module Initializers. Module Initializers are called only once, when the framework is started. This is our chance to inject the new logger to the framework.
However, that’s not that simple, we can’t just plug in the new logger to our framework and expect everything to work magically. We need to modify our logging function, so it provides a possibility to be replaced. For this reason, let’s define a new block that will enable this:
* Block that can be injected from the outside to override the default logger.
typedef void (^LogInjector)(LogLevel level, NSString *message);
The LogInjector will be nil by default and it can only be set from the outside. We will modify our logging function in order to reflect this. We are checking if there is a log injector, then execute that block, otherwise do the standard logging:
Now that we have this, we only need to get back to our Swift project and set the log injector in the startup of the framework. As mentioned before, the place where you need to do this is in the Module Initializer, in the main.m:
You can see now that we also have a new class LogHolder, which calls a method getLog. The reason we need this class is, that as you may have noticed, the StartupModuleInitializer is actually a C function, so we will need to find a way to inject that swift code in this C function. Let’s see how the LogHolder class might look like:
The getLog function provides a mapping between the C and Swift space. It returns a swift function, which just maps our customly defined LogLevel from our Objective-C framework, to the logging methods in the CleanroomLogger in Swift. This is accomplished by using C function pointers. From Apple’s documentation:
C function pointers are imported into Swift as closures with C function pointer calling convention, denoted by the @convention(c) attribute. For example, a function pointer that has the type int (*)(void) in C is imported into Swift as @convention(c) () -> Int32.
We are also adding the @objc to the class LogHolder, in order to be available in main.m.
And that’s all that needs to be done in order to inject the Swift code in a pure Objective-C framework at runtime. We will call the log function the same way we did in our Objective-C projects and only the logger implementation will be different for Swift projects.
You can use this technique to inject different things in your frameworks, depending on your project needs. Also, take note of the function pointer trick, because often you might need to go from Swift to C and the syntax to do that might look tricky at first.