In this article, we’ll cover what an XCFramework is, how to build one, and why we think XCFrameworks are great for PSPDFKit or any library vendor. We’ll also talk about the challenges we faced while adopting the new format at PSPDFKit.
The XCFramework format allows developers to conveniently distribute binary libraries for multiple platforms and architectures in a single bundle. For example, with XCFrameworks, vendors no longer need to merge (
lipo) multiple architectures into a single binary, only to later have to remove the Simulator slice during the archive phase.
XCFrameworks require Xcode 11 or later and they can be integrated similarly to how we’re used to integrating the
Building an XCFramework is a two-step process.
Archive the framework for each platform. Below, we’re archiving for the iOS device, Simulator, and Mac Catalyst architectures:
# Device slice. xcodebuild archive -workspace 'MyFramework.xcworkspace' -scheme 'MyFramework.framework' -configuration Release -destination 'generic/platform=iOS' -archivePath '/path/to/archives/MyFramework.framework-iphoneos.xcarchive' SKIP_INSTALL=NO # Simulator slice. xcodebuild archive -workspace 'MyFramework.xcworkspace' -scheme 'MyFramework.framework' -configuration Release -destination 'generic/platform=iOS Simulator' -archivePath '/path/to/archives/MyFramework.framework-iphonesimulator.xcarchive' SKIP_INSTALL=NO # Mac Catalyst slice. xcodebuild archive -workspace 'MyFramework.xcworkspace' -scheme 'MyFramework.framework' -configuration Release -destination 'platform=macOS,arch=x86_64,variant=Mac Catalyst' -archivePath '/path/to/archives/MyFramework.framework-catalyst.xcarchive' SKIP_INSTALL=NO
Create the XCFramework:
xcodebuild -create-xcframework -framework '/path/to/archives/MyFramework.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/MyFramework.framework' -framework '/path/to/archives/MyFramework.framework-iphoneos.xcarchive/Products/Library/Frameworks/MyFramework.framework' -framework '/path/to/archives/MyFramework.framework-catalyst.xcarchive/Products/Library/Frameworks/MyFramework.framework' -output '/output/path/MyFramework.xcframework'
Below, we highlight the main benefits of XCFrameworks over fat frameworks (.framework files with multiple architectures combined using
XCFramework is the officially supported format for distributing binary libraries.
Vendors can distribute binaries for multiple platforms (iOS, iPadOS, tvOS, and macOS) using a single XCFramework.
Unlike fat frameworks, XCFrameworks can contain multiple frameworks with the same architecture.
Compared to fat frameworks, integrating XCFrameworks is simple. For example, there’s no need for additional scripts to strip architectures when archiving for App Store submissions.
XCFrameworks allow developers to prepare for a world with an arm-based macOS version.
Before version 9, PSPDFKit for iOS was distributed as a fat framework. This meant we had to merge in the iOS device and Simulator architecture slices for each framework using the
lipo command. The fat framework format was never officially supported or endorsed by Apple, but it was the only way to distribute a single binary for both the iOS device and Simulator architectures.
With the introduction of Mac Catalyst, we could no longer use the
lipo command to merge the Mac Catalyst slice into a single fat framework. Here’s what happens when we try to do that:
lipo -create ./ios-arm64/PSPDFKit.framework/PSPDFKit ./ios-x86_64-maccatalyst/PSPDFKit.framework/PSPDFKit ./ios-x86_64-simulator/PSPDFKit.framework/PSPDFKit -output ./PSPDFKit fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/lipo: ./ios-x86_64-maccatalyst/PSPDFKit.framework/PSPDFKit and ./ios-x86_64-simulator/PSPDFKit.framework/PSPDFKit have the same architectures (x86_64) and can’t be in the same fat output file
This occurs because both the Simulator and Mac Catalyst use the same x86 architecture, and there can only be a single slice per architecture in a fat framework.
So the new XCFramework format came to the rescue! Instead of having to offer a single fat framework file for all architectures, the XCFramework format allows us to bundle a
.framework file for each architecture.
PSPDFKit is a complex project with a build system to match it. Our build system takes care of everything — from archiving and creating the XCFrameworks, to bundling all the necessary assets in the final DMG our customers download.
We not only had to update our entire build infrastructure, but we also had to make sure our three frameworks (PSPDFKit, PSPDFKitUI, and Instant) would build and archive on all three platforms that we support: iOS, iOS Simulator, and Mac Catalyst.
We anticipated all this work, and we managed to create the XCFrameworks with relative ease. However, we did not anticipate a few things that were not mentioned in the Apple documentation. Below, we will discuss the most notable challenges we encountered during the migration to XCFrameworks:
How are the bitcode and debug symbols handled in XCFrameworks?
How will popular dependency managers, like Carthage and CocoaPods, handle XCFrameworks?
How will the hybrid wrappers we support handle XCFrameworks?
Just like when building for release for the old fat framework format, archiving to build an XCFramework will generate both bitcode (
BCSymbolMaps) and debug (
.dSYM file) symbols if your framework is configured to generate them. Our previous build system handled both
dSYMs, and the required changes were minimal.
In the case of
BCSymbolMaps for PSPDFKit, they were automatically generated in the archive.
There was no additional step required for us to add the bitcode symbols to the resulting XCFrameworks.
The dSYMs were also part of the archive, but we had to rename each dSYM and bundle them all in an external folder, outside the XCFramework, like so:
|Archive dSYMs||DMG dSYMs|
When you integrate PSPDFKit into your project, neither the
BCSymbolMaps nor the
.dSYM files are automatically added to your app’s archive, so additional manual steps are required to add them. We automated part of this process with a build script. If you want to learn more about this, you can check out step 2 of our XCFramework integration guide.
CocoaPods did not support XCFrameworks when Xcode 11 came out, so we sponsored CocoaPods to add this support. It came in version 1.9.0, and as of PSPDFKit 9.4 for iOS, we offer XCFrameworks by default when integrating via CocoaPods.
At the time of writing, Carthage doesn’t yet support XCFrameworks. As a result, we still offer fat frameworks to allow our customers to continue integrating PSPDFKit on iOS via Carthage. Mac Catalyst is not supported in this case.
Most of the hybrid wrappers we support either already use CocoaPods or else allow developers to use it. For example, our Cordova/Ionic, Flutter, React Native, and Titanium plugins all use CocoaPods by default.
Certain wrappers — like Flutter, Appcelerator Titanium, and Xamarin — don’t support XCFrameworks yet. For such use cases where a project does not allow the use of XCFrameworks, we decided to continue offering the universal frameworks artifacts in CocoaPods via an alternate Podspec URL.
With PSPDFKit 9 for iOS, we started shipping Swift code inside PSPDFKit, and to support Swift module stability, we needed to archive our frameworks with
BUILD_LIBRARY_FOR_DISTRIBUTION enabled. This, in turn, also enables library evolution.
As @kamilpyc pointed out in their blog post, archiving with
BUILD_LIBRARY_FOR_DISTRIBUTION on changes how the code is compiled and could cause issues, like a corrupted generated
.swiftinterface file. We also experienced some build problems with this option enabled, so we initially shipped without it.
Fortunately, after a lot of experimentation and testing, we managed to avoid those issues, and we offer Swift module stability as of PSPDFKit 9.4 for iOS.
Despite some challenges and the shortcomings of the new format, we’re glad we migrated to XCFrameworks. We think XCFrameworks is the format of the future for distributing binary libraries in the Apple ecosystem.