Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quangDecember/9dbaf3d0c08850c8775900109ec87fdd to your computer and use it in GitHub Desktop.
Save quangDecember/9dbaf3d0c08850c8775900109ec87fdd to your computer and use it in GitHub Desktop.
Technical limitations when working with in Swift modular frameworks

Technical limitations when working with modularity in Swift frameworks

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.

Bypassing the access modifiers

So far this is the biggest challenge when working with Swift modularity.

Private Header with Objective-C

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

Dynamic Callable

@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

Swift language limitations

Extensions and properties

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 and other Framework limitations

Xcode is buggy. The only thing we can do about it is do it again manually. One of those is moving files between modules.

XCFramework

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

Clasify Assets

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.

Decision making and tips while attempting Swift framework modularity

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment