Recipe For Building a Closed Source Swift Framework With Dependencies
ABI compatibility issues aside, sometimes you may want to distribute a binary version of your Swift framework. This framework should be a ‘fat’ framework (supporting both simulator and iOS device architectures), and may depend on other third party frameworks. There is no Xcode project template for this, but you’ll find helpful information on the Internet. It basically comes down to using xcodebuild
to create two versions of your framework, one for the simulator and one for real iOS devices, and then using the lipo
command to combine them into a single fat framework.
It seems pretty straight forward, but after following instructions found online (examples here, here, and here), we experienced runtime crashes when using the binary version of our framework in an app. The crash would happen when our framework, called MyFramework
in the following example, would try to use a symbol from any third party framework dependency. The crash would look like this when MyFramework
tried to use RxSwift
's ObservableType
:
dyld: lazy symbol binding failed: Symbol not found: __TIFE7RxSwiftPS_14ObservableType9subscribeFT6onNextGSqFwx1ET__7onErrorGSqFPs5Error_T__11onCompletedGSqFT_T__10onDisposedGSqFT_T___PS_10Disposable_A0_Referenced from: /Users/aaron/Library/Developer/CoreSimulator/Devices/335F6B8A-0CBC-406D-9559–5718D4910F5A/data/Containers/Bundle/Application/47AA11E3-C2E5–4FCB-A86C-36092990EF0E/Consumer.app/Frameworks/MyFramework.framework/MyFrameworkExpected in: /Users/aaron/Library/Developer/CoreSimulator/Devices/335F6B8A-0CBC-406D-9559–5718D4910F5A/data/Containers/Bundle/Application/47AA11E3-C2E5–4FCB-A86C-36092990EF0E/Consumer.app/Frameworks/RxSwift.framework/RxSwift
After lots of investigation and experimention, we found a process that produces a fat framework that is crash-free when referencing symbols from other third party frameworks. It’s one of those things where I can’t totally explain what the problem was or why this works, but I wanted to share it here:
Create your framework
Use the Cocoa Touch Framework Xcode template, and feel free to depend on other frameworks however you choose (Cocoapods, Carthage, drag n’ drop, etc).
Create a target to build the fat framework
Use the Aggregate target type in the Cross-platform section. This creates a minimal target we can add a single run script build phase to. I’ll call this target BuildFatFramework
.
Add the fat framework building shell script to the target
Add a run script build phase to the BuildFatFramework
target, and add the following shell script to it:
# 1
# Set bash script to exit immediately if any commands fail.set -e# 2
# Setup some constants for use later on.FRAMEWORK_NAME="MyFramework"
OUTPUT_DIR="${SRCROOT}/build"# 3
# If remnants from a previous build exist, delete them.if [ -d "${OUTPUT_DIR}" ]; then
rm -rf "${OUTPUT_DIR}"
fi# 4
# Build the framework for device and for simulator (using
# all needed architectures).xcodebuild -workspace "${FRAMEWORK_NAME}.xcworkspace" -scheme "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos" -derivedDataPath "${OUTPUT_DIR}"xcodebuild -workspace "${FRAMEWORK_NAME}.xcworkspace" -scheme "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator" -derivedDataPath "${OUTPUT_DIR}"# 5
# Remove .framework file if exists from previous run.if [ -d "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework" ]; then
rm -rf "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework"
fi# 6
# Copy the device version of framework.cp -r "${OUTPUT_DIR}/Build/Products/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework"# 7
# Replace the framework executable within the framework with
# a new version created by merging the device and simulator
# frameworks' executables with lipo.lipo -create -output "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${OUTPUT_DIR}/Build/Products/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${OUTPUT_DIR}/Build/Products/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"# 8
# Copy the Swift module mappings for the simulator into the
# framework. The device mappings already exist from step 6.cp -r "${OUTPUT_DIR}/Build/Products/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"
Customize this script as needed to fit your project.
Initiate the build from the BuildFatFramework target
Now you can build your fat framework by running the BuildFatFramework
target of your Xcode project.
Very important: If buiding from the command line, be sure to use xcodebuild
, specifying BuildFatFramework
as the scheme and build
as the action (not archive
which is the default if you don’t specify an action). This was the key step that led to our crash-free framwork.
The archive
action is not correct, and additionaly, it seems like Xcode is doing something to properly set up the build environment when you build the BuildFatFramework
scheme. Saving the contents of the shell script that builds the fat framework to a stand alone file, and running it directly, bypassing the BuildFatFramework
scheme, resulted in a framework that crashed.
Here is the command we use in our CI system to initiate the building of the fat framework:
xcodebuild -scheme BuildFatFramework -workspace MyFramework.xcworkspace build
Hopefully Xcode will provide better support for building fat frameworks in the future, but until then, I hope this information can be useful to someone out there.