As iOS developers, we’ve long faced a difficult choice when architecting our apps: should we use static or dynamic frameworks? Each option comes with significant trade-offs that impact both developer productivity and end-user experience.
With the introduction of mergeable libraries in Xcode 15, Apple has given us a solution that combines the benefits of both approaches. In this article, I’ll explain what mergeable libraries are, how they solve common problems in modular iOS applications, and how to implement them in your projects.
The Static vs. Dynamic Framework
Before diving into mergeable libraries, let’s review the trade-offs between static and dynamic frameworks:
Static Libraries
- Pros: No runtime overhead, faster app launch times
- Cons: Increased build times, larger app binaries, slower incremental builds
Dynamic Frameworks
- Pros: Faster build times, smaller app binaries, better for incremental builds
- Cons: Higher launch time cost as frameworks must be loaded at runtime
This creates a classic dilemma: optimize for user experience (static libraries) or developer experience (dynamic frameworks)?
Enter Mergeable Libraries
Introduced at WWDC 2023 and available from Xcode 15 onward, mergeable libraries offer a compelling solution by providing the best of both worlds:
- In debug builds, dependencies behave as dynamic frameworks, providing quick build times for developers
- In release builds, dependencies are merged into the main app binary, similar to static linking, ensuring fast launch times for users
Common Problem: The Dynamic Framework Dependency Chain
To understand how mergeable libraries solve real problems, let’s consider a modular app structure with a dependency chain:
Imagine an app that depends on a App
dynamic framework, which in turn depends on internal frameworks AppCore
and AppUI
. The naive approach would be:
App (dynamic framework, linked and embedded)
├── AppCore (dynamic framework, linked but not embedded)
└── AppUI (dynamic framework, linked but not embedded)
When you run this configuration on a device, you’ll likely see a crash with errors like:
dyld: Library not loaded: @rpath/AppCore.framework/AppCore
...
dyld: Library not loaded: @rpath/AppUI.framework/AppUI
Traditional Solution
To fix this, you would need to:
- Link and embed
AppCore
andAppUI
frameworks in the main app target - Link (but don’t embed) these frameworks in the
App
target
This approach works but has several drawbacks:
- Your frameworks are no longer self-contained
- You expose
AppCore
andAppUI
interfaces to the app target unnecessarily - Each dynamic framework adds launch time overhead
Implementing Mergeable Libraries
Mergeable libraries eliminate these issues with an elegant solution. You have two approaches for implementation:
1. Automatic Merging
The simplest approach is to set the Create Merged Binary
build setting to Automatic
in your target:
- For the
App
framework, setMERGED_BINARY_TYPE = Automatic
- For the app target, also set
MERGED_BINARY_TYPE = Automatic
With automatic merging, Xcode will:
- Merge all direct dependencies into the target’s binary
- Handle the merging process automatically
2. Manual Merging
For more fine-grained control:
- Set
Create Merged Binary
toManual
in the consuming target - For each dependency you want to merge, set
Build Mergeable Library
toYes
For example, in our sample app:
In the App framework:
- Set
Create Merged Binary
toManual
- Set
Build Mergeable Library
toYes
for bothAppCore
andAppUI
In the App target:
- Set
Create Merged Binary
toManual
- Set
Build Mergeable Library
toYes
for theApp
framework
Debug vs. Release Behavior
An important detail to understand is how mergeable libraries behave differently in debug versus release builds:
Debug Builds
- Dependencies are treated as dynamic frameworks
- Frameworks are reexported to a directory called
ReexportedBinaries
in the app bundle - This optimizes for faster build times during development
Release Builds
- Dependencies are merged into the main binary
- No separate framework files exist in the bundle
- This optimizes for faster launch times in production
You can verify this by inspecting your app bundle in the derived data folder for both debug and release builds.
Optimizing Binary Size
While mergeable libraries are already optimized for size, there is a slight overhead due to additional metadata. You can further optimize by:
- Setting
-Wl,-no_exported_symbols
in theOther Linker Flags
build setting of your app target to strip exported symbols - For more control, consider providing an export list file instead to retain only necessary symbols
Testing Mergeable Libraries
You can verify that your configuration works by:
- Using
otool -L
on your app binary to see what dynamic libraries it depends on - Using
nm -gU
to inspect the embedded symbols
In a properly configured setup with mergeable libraries in release mode:
otool -L
should only show system frameworks, not your merged librariesnm -gU
should show symbols from your merged libraries embedded in the app binary
Important Considerations
- Make sure to remove frameworks from the “Embed Frameworks” phase if they are being merged; otherwise, they’ll be treated as normal dynamic frameworks
- The
-no_exported_symbols
linker option might not work in all scenarios; test thoroughly - You need Xcode 15 or later to use this feature
Conclusion
Mergeable libraries represent a significant advancement in iOS app development, allowing us to optimize both build time and launch time performance without compromise. By behaving like dynamic frameworks during development and static libraries in production, they provide the best of both worlds.
As you modernize your iOS projects, consider adopting mergeable libraries, especially for modular apps with complex dependency chains.