Right before WWDC 2020, we wrote about supporting XCFrameworks, and it feels like Apple listened and delivered all the items on our wish list:
Binary frameworks as Swift packages
Automatic support for Apple Silicon via fat binaries
Built-in support for the BCSymbolMaps and dSYMs
In this article, we’ll go over the recent advances in the XCFramework format in Xcode 12, and then we’ll discuss how these changes impacted us as an SDK vendor.
💡 Tip: To learn the basics of XCFrameworks and how to build them, we recommend reading our Supporting XCFrameworks blog post first.
Before discussing the new features and enhancements for XCFrameworks introduced with Xcode 12 and iOS 14, we’d like to congratulate and thank Apple for fixing several bugs quickly, allowing us to offer the best possible support for XCFrameworks. Here are a few resolved feedback reports:
FB7786238 — XCFrameworks should bundle and handle debug symbols that will be added to the app’s archive
FB7785691 — The archived app’s Frameworks directory does not contain the binary frameworks from Swift Package Manager dependencies
FB8761306 — Binary SwiftPM frameworks are copied (duplicated) into share extensions PlugIns directory causing the archive validation to fail (fixed in Xcode 12.5 beta)
At WWDC 2020, Apple announced binary framework support in Swift Package Manager. PSPDFKit SDKs were already built as XCFrameworks, so it was relatively easy to offer PSPDFKit as a Swift package. We wrote about this topic on our blog in the Binary Frameworks as Swift Packages post.
With Xcode 12, Apple added support for Apple Silicon, and since PSPDFKit 10.1 for iOS, we’ve been offering Apple Silicon architectures for our iOS Simulator and Mac Catalyst.
ℹ️ Note: Only the Simulator and Mac Catalyst binaries are fat. The iOS device binary contains only the arm64, like before, which explains why iOS apps can run on M1 Macs and not on Intel Macs.
The Apple Silicon architectures are automatically generated for the framework binaries and its dSYMs when the framework is archived.
In our case, we didn’t have to update our archive
xcodebuild command for Xcode 12. Here’s an example showing how to archive a sample framework for an iOS device, Simulator, and Mac Catalyst:
# 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
You can verify that the framework binary from the archive is fat for Simulator or Mac Catalyst slices using the
lipo command, like so:
lipo -info /path/to/archives/MyFramework.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/MyFramework.framework/MyFramework
The output should look like this:
Architectures in the fat file: /path/to/archives/MyFramework.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/MyFramework.framework/MyFramework are: x86_64 arm64
Before Xcode 12, the archive phase generated the debug symbols, but they didn’t make it into the final XCFramework. So we had to bundle them into separate folders, which complicated our release build scripts. This meant our customers needed to perform additional integration steps to ensure that both the BCSymbolMaps and dSYMs were copied into their app’s archive.
With XCFrameworks created with Xcode 12, this is no longer the case: Both the BCSymbolMaps and the dSYMs will automatically be added to the final XCFramework, and Xcode 12 will automatically pick them up during the app archive step.
For this to happen, we’re taking advantage of the newly added
-debug-symbols parameter for the
xcodebuild command. We now pass the path of each debug symbol from the framework’s archive. The updated
xcodebuild command for Xcode 12 should look like this:
xcodebuild -create-xcframework -framework '/path/to/archives/MyFramework.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/MyFramework.framework' \ -debug-symbols '/path/to/archives/MyFramework.framework-iphonesimulator.xcarchive/dSYMs/MyFramework.framework.dSYM' \ -framework '/path/to/archives/MyFramework.framework-iphoneos.xcarchive/Products/Library/Frameworks/MyFramework.framework' \ -debug-symbols '/path/to/archives/MyFramework.framework-iphoneos.xcarchive/dSYMs/MyFramework.framework.dSYM' \ -debug-symbols '/path/to/archives/MyFramework.framework-iphoneos.xcarchive/BCSymbolMaps/BCSymbolMaps_UUID.bcsymbolmap' \ -framework '/path/to/archives/MyFramework.framework-catalyst.xcarchive/Products/Library/Frameworks/MyFramework.framework' \ -debug-symbols '/path/to/archives/MyFramework.framework-catalyst.xcarchive/dSYMs/MyFramework.framework.dSYM' \ -output '/output/path/MyFramework.xcframework'
ℹ️ Note: The
BCSymbolMaps_UUIDvariable from the
xcodebuildcommand above is a UUID generated every time the framework is archived for the iOS device architecture. To learn more about calculating the UUID of the BCSymbolMaps for an XCFramework binary, refer to this code snippet from our Rakefile in our PDFXKit open source repository.
Here’s a comparison showing how the PSPDFKit XCFramework built with Xcode 11 and Xcode 12 look on disk:
|XCFramework Built with Xcode 11||XCFramework Built with Xcode 12|
And here’s a comparison showing how we bundled the dSYMs for PSPDFKit when we used to build the PSPDFKit XCFramework with Xcode 11 and how we bundle them now with Xcode 12:
|Xcode 11 dSYMs||Xcode 12 dSYMs|
Before the new Xcode 12 XCFramework format, CocoaPods handled the BCSymbolMaps and dSYMs using phase scripts, and it wasn’t ideal. CocoaPods copied all the dSYMs (including dSYMs for invalid architectures) into the app archive, resulting in a needlessly bloated app archive. For example, in the screenshots below, you can see that all the dSYMs, including Simulator and Mac Catalyst dSYMs, used to be copied over into the iOS app’s archive:
|Xcode 11 Format dSYMs in Archive||Xcode 12 Format dSYMs in Archive|
As of version 1.10.0 (see CocoaPods pull request #10122), CocoaPods no longer handles the dSYMs when using the new XCFramework format. Instead, CocoaPods will let Xcode take care of the debug symbols, just like Swift Package Manager and the manual XCFramework integration.
Earlier this month, Carthage released version 0.37.0, which adds support for XCFrameworks on iOS but not on Mac Catalyst (which remains unsupported in Carthage). As a result, we still offer fat frameworks to allow our customers to continue integrating PSPDFKit on iOS via Carthage.
Since we published the initial blog post about supporting XCFrameworks, the state of hybrid technologies has changed: All the hybrid technologies we support (Cordova/Ionic, Flutter, and React Native), with the exception of Xamarin, use CocoaPods by default and fully support XCFrameworks.
Xamarin recently merged its pull request that adds support for XCFrameworks, but it’s not available in the latest stable version. For now, our Xamarin bindings still rely on the old fat framework format.
Since the introduction of the XCFramework format at WWDC 2019, XCFrameworks have evolved and improved considerably. We hope and expect to see more improvement at WWDC 2021. On the top of our wish list for WWDC 2021 is FB7787327 (Allow the conditional inclusion/exclusion of resources in Swift Packages), which would let us offer better Swift Package Manager support for OCR languages.
We believe XCFrameworks is the format of the future for distributing binary libraries in the Apple ecosystem.