The following Gist list the technical challenges and some decision making while attempting to updating a binary frameworks to a modular architecture with multiple binary frameworks.
So far this is the biggest challenge when working with Swift modularity.
Private Header is one of the good old way to share code between 2 frameworks without exposing the API to app developers.
The model I was trying to use:
@objc internal class AA
in Swift- Overrides the generated header of the class by using
SWIFT_CLASS(AA)
in my own header AA.h - Add the new header you created as Private header to FrameworkA
- Call that header from FrameworkB
When coming to Swift framework, the first limitation is No Briding Header is allowed in Swift framework. Because of that limitation, I decided to make a private modulemap.
- Create a module call FrameworkBPrivate
- Adding (
#import
) other Objective-C headers within the framework to umbrealla header of that private framework. import FrameworkBPrivate
in FrameworkB
ModuleMap example:
module FrameworkBPrivate {
header "../FrameworkB-Briding-Header.h"
export *
}
Using modulemap is a very effective way to bypass the briding header limitations in Swift framework. However in our case, we have to face another limitation: AA.h is not usable in FrameworkB. (Despite it's usable in AA test target).
Verdict: FAILED
@dynamicCallable
is a new Swift features introduced in Swift 5. The feature was intended for calling Python and Ruby from Swift but it is helpful in our case as well:
First of all, mark the class you want to hide some APIs from your app developer with @dynamicCallable
:
@dynamicCallable
public class AA
Next, implement one of two following functions:
public func dynamicallyCall(withArguments args: [String]) -> Any? {}
public func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {}
Parameters must conform to Array Literal or Dictionary Literal. There is no rule on return type.
For example, to access reference property var AAViewController: ViewController
you return it in one of the above functions. Identification of what you return is based on the String
you pass as a parameter of dynamicallyCall(withArguments:)
from Framework B.
Unfortunately, @dynamicCallable
is not usable for extensions.
Verdict: partially SUCCESSFUL
NO stored property in extension: Yes, Stored Property isn't allowed in Extensions. So there's no way you can add a data member to class AA above without subclassing, which is not helpful in many cases such as a AA singleton from FrameworkA.
Xcode is buggy. The only thing we can do about it is do it again manually. One of those is moving files between modules.
On XCFramework side, no submodule and no umbrella framework rule makes it impossible to hide some utilities function without using @dynamicCallable
List of other XCFramework issues
If your Assets.xcasset is huge, it's almost impossible to detect which image/color/... would be used in which module. Duplicating the asset catalog may be inevitable.
Questions when making a Swift binary framework modular:
- Which class, struct or enum should be public?
- For utilities shared between modules, should we move it to Core module or duplicate it among frameworks?
- Sharing mock data between network tests, UI tests, integration tests (which lies among targets) without duplicating?
Complicated separating: Storyboard files.