Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
Splitting Your Xcode Project into Modules
Feb 6, 2020
8 minutes read

Update on 2020-03-05

Apparently the -undefined dynamic_lookup flag I mention at the bottom of this post is incompatible with bitcode. If you use that flag on a dynamic framework that has bitcode enabled you’ll get a build error with the latest version of Xcode.

-undefined and -bitcode_bundle (Xcode setting ENABLE_BITCODE=YES) cannot be used together

The obvious solution would be to disable bitcode. But Apple already mandates this for watchOS and tvOS apps. So the chances they require this for iOS in the future are pretty high. This sounded like a deal-breaker when I first encountered the issue, but the solution is pretty simple.

You actually don’t need to use those flags if you’re careful with your dependency graph. If, like me, you statically link all your modules (static frameworks) into a core dynamic framework you share between all your executable targets, you just need to have all your other dynamic frameworks link to that core framework instead.

To put it in other words, all of your dynamic frameworks that depend on those modules must link the core dynamic framework that contains them. That’s it.

For a while now in every new feature I’m building into Secrets, I’m defaulting to using Swift.

One of the things Swift brings to the table is access control. Using the public, private, internal, etc, modifiers is a great way to document your intent and vend a clean API. But to be effective, these features need to be encapsulated into separate modules.

Maintainable software comes from writing small cohesive highly intra-connected components with few inter-connections between them. Using modules forces you to think about the responsibilities of each one, and makes it harder to break abstractions. They also improve reusability and testability.

I was convinced this was the way to go but…

How should I split my project to accommodate this? I’m sure there’s an easy solution.

A naive Paulo

Well, I just spent two days1 on this 😳.

Swift Packages

Now that Xcode has support for Swift Packages this was my first stop. But I didn’t stay long…

Swift Packages are great if your starting a new Swift-only project or building something for Linux as well. In my case, Secrets is +90% Objective-C, with first and third-party frameworks (using Carthage) and a bunch of .xcconfig files. Trying to write a Package.swift that would accommodate all this was enough to put this idea aside.

Besides, there wasn’t any real advantages of using Swift Packages in my case. There’s nothing a Swift Package can do that I can’t using Xcode’s library and framework targets.

Library Targets

You can think of libraries (static or dynamic) as a collection of code objects (like books in a library). Unlike frameworks, libraries don’t have a built-in way to associate resources such as .strings, images, etc. But for most of these features this wouldn’t be a problem as they are usually code only.

The problem with these targets is that they don’t define a module by default. And while it’s easy to fix this for Swift simply by setting the DEFINES_MODULE build setting to YES, this alone isn’t enough to expose the module to Objective-C. To do that, you’d need to create a module map and correctly place the generated Objective-C compatibility header in a custom “Run Script” phase.

While not terribly difficult, having to do so for every new target seemed like too much. Specially considering that frameworks have all this sorted out.

Framework Targets

Like libraries, frameworks can be dynamic or statically linked. By default Xcode creates dynamic frameworks but you can easily change that using the MACH_O_TYPE build setting.

Changing the framework Mach-O type

Changing the framework Mach-O type

Dynamic Frameworks

Dynamic frameworks are undoubtably the most common, but if you’ve been reading some performance blog posts you already know they can seriously hinder your app’s startup time.

As of writing Secrets already includes 15 dynamic frameworks! And this is excluding Swift’s dynamic libraries. By using dynamic frameworks to encapsulate these features, this number would certainly increase drastically when it should be decreasing instead.

You can reduce your app’s launch time by limiting the number of frameworks you embed.

— Apple in Identify Launch-Time Factors: Dynamic Library Loading

Although devices keep getting faster, I definitely didn’t feel like starting to split up my project knowing I could eventually have to deal with slow app launches.

Static Frameworks

Static frameworks solve both problems mentioned above. They automatically define a module usable from Swift and Objective-C and their code is included in your final binary at compile time. So there’s no need to do dynamic symbol resolution at startup for them. You don’t even need to embed these frameworks in you app bundle.

And if all your frameworks are static then you’re all set. But more often than not you’ll want to have one or more dynamic frameworks. Either because you need to bundle them with resources or you don’t want to duplicate your code in all the executables you’re shipping (with all the extensions an iOS app can bundle nowadays this can add up).

The problem with mixing dynamic and static frameworks is that if they depend on each other, you can easily run into to duplicate symbols issues2.

objc[49102]: Class _TtC6Static13StaticSymbolC is implemented in both /Redacted/Path/StaticFrameworks-dvmjkolrxuxdzkcjfczlotmecujq/Build/Products/Debug/DynamicB.framework/Versions/A/DynamicB (0x1003903b8) and /Redacted/Path/StaticFrameworks-dvmjkolrxuxdzkcjfczlotmecujq/Build/Products/Debug/DynamicA.framework/Versions/A/DynamicA (0x1003835b0). One of the two will be used. Which one is undefined.

Before starting to slice and dice my project up, I wanted to know if there was a way around this.

Hypothetical duplicate symbols example. Solid arrows mean symbols copied, dashed arrow mean symbols are simply referenced.

Hypothetical duplicate symbols example. Solid arrows mean symbols copied, dashed arrow mean symbols are simply referenced.

So I dug deeper in the rabbit hole and created a test project to come up with a solution. After reproducing the issue I tried a bunch of different tactics like messing with the link order and whatnot… but they were all a dead end.

Taking a step back, and looking a drawing I had made of the picture above, I figured I just needed to turn those solid arrows between the dynamic and static frameworks to be dashed. This way they would just reference the symbols but not include them, since they can be included when linking the final executable.

Dynamic frameworks only reference static symbols but don't include them.

Dynamic frameworks only reference static symbols but don't include them.

Or putting it in ld terms, we need all symbols from the static frameworks to remain undefined on all other frameworks.

Easier said than done.

First of all, Xcode will write all your targets to your built products dir. And it will also include this directory on the framework search path. This means that when building the dynamic frameworks the linker will find the static frameworks that are already in the built products dir and include any referenced symbol automatically.

I didn’t find a way to tell Xcode not to include the built products dir on the framework search path but I did find a workaround: I changed the built products dir for the static frameworks target to be a sub-dir called “Static”.

CONFIGURATION_BUILD_DIR = "$SYMROOT/$CONFIGURATION/Static"

This prevents the linker from finding static frameworks when compiling another framework. But it also means it can’t find the modules these frameworks define…

To fix that, we need to copy both the .swiftmodule, the module map and headers from the framework, neatly pack them, and place them somewhere Xcode can find. I decided to place these in a “Modules” directory. Adding a “Run Script” phase to our static framework targets can take care of that:

MODULES_DIR="$CONFIGURATION_BUILD_DIR/../Modules"
OBJC_MODULE_DIR="$MODULES_DIR/$PRODUCT_MODULE_NAME"
SWIFT_MODULE_DIR="$MODULES_DIR/$PRODUCT_MODULE_NAME.swiftmodule"
[ -d "$OBJC_MODULE_DIR" ] && rm -rf "$OBJC_MODULE_DIR"
[ -d "$SWIFT_MODULE_DIR" ] && rm -rf "$SWIFT_MODULE_DIR"

mkdir -p "$MODULES_DIR" 

# copy swift module
cp -R "$CONFIGURATION_BUILD_DIR/$MODULES_FOLDER_PATH/$PRODUCT_MODULE_NAME.swiftmodule" "$MODULES_DIR/" 

# copy/make objc module
mkdir -p "$OBJC_MODULE_DIR"
cp "$CONFIGURATION_BUILD_DIR/$MODULES_FOLDER_PATH/module.modulemap" "$OBJC_MODULE_DIR/"
cp -R "$CONFIGURATION_BUILD_DIR/$PUBLIC_HEADERS_FOLDER_PATH" "$OBJC_MODULE_DIR/"

And let all the targets know where to find these modules:

HEADER_SEARCH_PATHS = $CONFIGURATION_BUILD_DIR/Modules # ObjC needs this
SWIFT_INCLUDE_PATHS = $CONFIGURATION_BUILD_DIR/Modules # Swift needs this

Our built products dir now looks like this:

build/Debug
├── DynamicA.framework
├── DynamicB.framework
├── Modules
│   ├── StaticModuleA               # ObjC will use this
│   │   ├── Headers
│   │   │   └── StaticModuleA-Swift.h
│   │   └── module.modulemap
│   └── StaticModuleA.swiftmodule.  # Swift will use this
├── Static
│   └── StaticModuleA.framework
├── YourApp.app

You can now import these modules and have autocompletion work as expected. But compiling will still produce an error because the linker won’t be able to find the static frameworks. But we were already expecting that. We need to tell the linker not to treat undefined symbols as errors.

OTHER_LDFLAGS = -Xlinker -undefined -Xlinker dynamic_lookup

What the above setting does is tell the linker to treat any undefined symbols as dynamic.

Finally we just need to make sure the final executable target knows where to find the static frameworks so the linker can do its job and pull in any needed symbols.

FRAMEWORK_SEARCH_PATHS = $SYMROOT/$CONFIGURATION/Static

Done. The project now builds and runs with no issues or warnings related to duplicate symbols 🥳.

Did I complicate this too much? Let me know on Twitter!


  1. I consider myself a Griffin in this regard. [return]
  2. You could always ignore the warnings… but I’m too pedantic for that. [return]


Back to posts