Creating Swift framework with private Objective-C members.
The Good, the Bad, and the Ugly
I was recently working on a closed source Swift framework. Unluckily, some parts of the code were in Objective-C (relying on pure C libraries). In this article, I highlight some of the problems when having frameworks with mixed Swift/ObjC code. I show some approaches I tried when struggling to make ObjC code internal. Making it invisible to the framework users, but still accessible from the Swift code gave me a severe headache. Also, I share a solution that finally worked out in my case.
There are a lot of articles and tutorials about Swift/ObjC interoperability, but they rarely focus on framework targets. It seems that even when you have everything in place, it is still close to impossible to effectively hide your Objective-C part from framework users (at least as long as you want to expose it to the Swift part).
Let’s assume you created a framework, named MyFramework, with following members:
Our Swift class depends on Objective-C class, so it needs to know about it somehow. However, we don’t want the client app to be able to see our ObjC internals:
Seems easy, right? Well, it is actually harder than it looks.
- we have a mixed Objective-C, C (or C++), Swift framework target
- we need to expose Objective-C members to Swift code internally
- we don’t want framework users, to see our internal ObjC members
1. The Good — yet not working
By good, I mean “default” Swift ObjC interoperability. Just for a short recap:
To expose Swift to Objective-C:
- target needs to define a module
- Mark Swift members as @objc
- Make Swift members inherit from NSObject
To expose Objective-C to Swift:
- place ObjC imports in bridging header file (if available)
- place ObjC imports in umbrella header (for frameworks)
You can read more about it here:
- Apple: Importing Swift into Objective-C
- Apple: Importing Objective-C into Swift
- Jen Sipila article about Swift interoperability
There is one significant problem with this approach:
“Swift sees every header you expose publicly in your umbrella header. The contents of the Objective-C files in that framework are automatically available from any Swift file within that framework target, with no import statements.”
So it does not provide the desired behavior. Moreover, I noticed that quite often it leads to a chain of “including non-modular header” issues. That literally makes you rewrite half of the headers. It is still doable, as long as you don’t import any header that is out of your control, for example, headers from some static library. Summing up:
- Its intended way to do ObjC -> Swift interoperability
- All Objective-C members exposed to Swift are also publicly visible
- Common issues with importing non-modular headers
2. The Bad - a private module maps that almost work
When searching for a possible solution, this is one of the most popular. It relies on creating a private modulemap file, that defines explicit submodule. I don’t want to get too much into details on how to do it, here are some useful links:
- stackoverflow: How to import private framework headers in a Swift framework?
- Realm Academy
- Omar Abdelhafith blog
In quick words — you can manually define an explicit submodule with private header files, named for example MyFramework.Private. Then you access it from your Swift code as following:
It is not very private though as it would also work for the framework user:
Framework users still can access “private” members. It is a bit harder, since importing the main module does not reveal “private” members immediately. It also requires an additional import statement to access “private” members. You can emphasize that it is not safe nor intended to use it. However, you cannot hide it.
To sum it up:
- It gives some control over what is visible to the user by default
- Allows to emphasize, that “private” submodule is for internal use only and it should not be used directly
- All Objective-C members exposed to Swift are still public
- It requires manual module maps, which are harder to set up, and easier to misuse
The Ugly — a solution that works
In my case, the modulemaps were not enough. The research output wasn’t promising. It seemed that there is no way to expose Objective-C headers privately, inside the same framework target only, without making them more or “less” public. So it is true so far.
You might wonder what I did have in mind then when writing about finding a solution that works. Let me share a part of a discussion I had with myself:
Me: “Ok, let’s think again, what do you want to achieve?”
Me2: “I want to expose Objective-C headers to Swift, but only inside the framework target, without exposing them to the framework users.”
Me: “Do you? Is that exactly what you need?”
Me2: “Well, I have ObjC classes that are meant to be internal, but I want to use them from Swift code.”
Me: “OK. How would you actually ‘use’ this classes?”
Me2: “Call some methods, access variables?”
Me: “Do you need to ‘know the class’ to do that?”
Me2: “Well, I only need to know that there is a method or a variable, so protocol should be fine.”
Me: “And if you invert the problem? Can you have an internal Swift member and use it from Objective-C.”
Me2: “It seems that the answer is yes, with some restrictions of course.”
Me: “OK. Can you make then the private Objective-C member adopt an internal Swift protocol?”
Me2: “Seems so… Does it mean I don’t need to know about any Objective-C class, as I interact only with things adopting Swift protocols?
Me: “Well, let’s try.”
Step 1: Create a Swift protocol matching ObjC member
Let’s update the framework a bit according to that idea. The swift code does not have to see the Objective-C part at all. However, it can operate on anything adopting protocol matching our ObjC class features:
Step 2: Expose internal Swift protocol
MyPrivateClassProtocol is internal, it would not be a part of
MyFramework-Swift.h by default. So let's created an additional header for linking all our internal Swift and ObjC members:
Note: If you are unsure how should you fill this header, you can make Swift members public and build. That generates ‘ModuleName-Swift.h’ header in derived data. Inspecting it should give you some sense of how to do it. Then make it internal again.
Step 3: Adopt Swift protocol in ObjC
Now as we can see Swift protocol in Objective-C as
MyPrivateClassProtocol, we can adopt it.
That leaves us with one last final problem…
Step 4: How to create instances
Me2: “OK, I theoretically can use it. But again, how do I get an instance of it if I don’t know about a class adopting protocol?”
Me: “Remember that factory pattern?”
The problem with the solution above is that we just cannot instantiate a new instance of a Protocol without using the
init on a concrete class, can we? Let's consider this:
Let’s focus on the last part. We can create a helper factory class, that holds
Note: In Swift part, MyPrivateClass is a protocol, while in ObjC it is a class. It might seem ambiguous, but I wanted to keep same names on both sides.
Factory above would be exposed to Objective-C in the same way as the
MyPrivateClass Protocol definition (so internally).
The one last thing that needs to be done is to register the Objective-C class to be used by the factory. We need to do it before we could use it.
Luckily, Obj-C runtime has something just up to the job:
So it seems, that we can use Objective-C members in Swift without exposing ObjC code to Swift at all! All we need to do is to expose the Swift part to Objective-C, which we can do internally 😎.
Let’s sum it up:
- It effectively hides Objective-C members from framework users
- You are forced to interface stuff. It is usually a good thing, increasing overall testability
- You need some additional members to cross Swift-ObjC boundary
- It’s a manual job to generate Swift protocol for every ObjC member
The example project is available on the GitHub:
The whole approach requires some manual work and forces to redesign how you initialize your dependencies slightly. You need to generate protocols, manually bridge it to ObjC, and also maintain factory along the way. However, this is the only way so far have genuinely internal Objective-C implementation and use it in the Swift code.
Note: In the example, I used the factory that allows you to register a type, and then I used init on that type. You can play with this approach a bit. If you don’t like to add inits to protocols, you can try to use closure/block as a factory:
static var createPrivateClass: (() -> MyPrivateClass)!
Thank you for reading
If you liked it, feel free to 👏
Special thanks to Timothy Anderson, for allowing me to use his illustrations in this article.
Originally published at blog.girappe.com.