6 min read
Mergeable Libraries in iOS

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:

  1. Link and embed AppCore and AppUI frameworks in the main app target
  2. 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 and AppUI 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:

  1. For the App framework, set MERGED_BINARY_TYPE = Automatic
  2. 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:

  1. Set Create Merged Binary to Manual in the consuming target
  2. For each dependency you want to merge, set Build Mergeable Library to Yes

For example, in our sample app:

In the App framework:

  • Set Create Merged Binary to Manual
  • Set Build Mergeable Library to Yes for both AppCore and AppUI

In the App target:

  • Set Create Merged Binary to Manual
  • Set Build Mergeable Library to Yes for the App 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:

  1. Setting -Wl,-no_exported_symbols in the Other Linker Flags build setting of your app target to strip exported symbols
  2. 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:

  1. Using otool -L on your app binary to see what dynamic libraries it depends on
  2. 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 libraries
  • nm -gU should show symbols from your merged libraries embedded in the app binary

Important Considerations

  1. Make sure to remove frameworks from the “Embed Frameworks” phase if they are being merged; otherwise, they’ll be treated as normal dynamic frameworks
  2. The -no_exported_symbols linker option might not work in all scenarios; test thoroughly
  3. 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.

Further Resources