Distributing Multi-arch SDKs for Apple silicon (M1) and Intel-based Platforms
Apple silicon Macs have arrived and are becoming more common in the iOS development community. This change means that iOS SDKs will need to support both M1 and Intel architectures so that code can be run on both types of iOS simulators. To accomplish this, you'll need to update your binary files from traditional frameworks to XCFrameworks.
The Introduction of XCFramework and Xcode 11
In the past, SDKs have used the lipo command to create “fat" frameworks by bundling binary builds together for iOS devices, iOS Simulators, Mac Catalyst, and Macs. However, fat frameworks built with lipo don't support multiple architectures for the same build target.
To overcome this restriction, Apple introduced the XCFramework binary file type with Xcode 11. XCFrameworks can be used instead of traditional iOS Frameworks to support multiple architectures for the same build target, as well as multiple build targets that use the same architecture.
How to Build an XCFramework Using a Bash Script:
- Create archives for each of the platforms you want to support and the architecture of your choosing.
- Make sure to add the new xcodebuild option:
“BUILD_LIBRARIES_FOR_DISTRIBUTION”
- To support M1 and Intel architectures, set the target's destination to "generic/platform=TARGET_PLATFORM_HERE"
-destination="generic/platform=iOS Simulator"
- Once the archives are created, use the xcodebuild command
-create-xcframework
followed by the frameworks you wish to be included in the binary.
#!/bin/bash
set -e
WORKING_DIR=$(pwd)
FRAMEWORK_FOLDER_NAME="OneSignal_XCFramework"
FRAMEWORK_NAME="OneSignal"
FRAMEWORK_PATH="${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}/${FRAMEWORK_NAME}.xcframework"
BUILD_SCHEME="OneSignalFramework"
SIMULATOR_ARCHIVE_PATH="${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}/simulator.xcarchive"
IOS_DEVICE_ARCHIVE_PATH="${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}/iOS.xcarchive"
CATALYST_ARCHIVE_PATH="${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}/catalyst.xcarchive"
rm -rf "${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}"
echo "Deleted ${FRAMEWORK_FOLDER_NAME}"
mkdir "${FRAMEWORK_FOLDER_NAME}"
echo "Created ${FRAMEWORK_FOLDER_NAME}"
echo "Archiving ${FRAMEWORK_NAME}"
xcodebuild archive ONLY_ACTIVE_ARCH=NO -scheme ${BUILD_SCHEME} -destination="generic/platform=iOS Simulator" -archivePath "${SIMULATOR_ARCHIVE_PATH}" -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES
xcodebuild archive -scheme ${BUILD_SCHEME} -destination="generic/platform=iOS" -archivePath "${IOS_DEVICE_ARCHIVE_PATH}" -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES
xcodebuild archive -scheme ${BUILD_SCHEME} -destination='generic/platform=macOS,variant=Mac Catalyst' -archivePath "${CATALYST_ARCHIVE_PATH}" SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES
xcodebuild -create-xcframework -framework ${SIMULATOR_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework -framework ${IOS_DEVICE_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework -framework ${CATALYST_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework -output "${FRAMEWORK_PATH}"
rm -rf "${SIMULATOR_ARCHIVE_PATH}"
rm -rf "${IOS_DEVICE_ARCHIVE_PATH}"
rm -rf "${CATALYST_ARCHIVE_PATH}"
open "${WORKING_DIR}/${FRAMEWORK_FOLDER_NAME}"
Distributing Your XCFramework Using Swift Package Manager and Xcode12
When XCFrameworks were first introduced by Apple, they were incompatible with Swift Package Manager. Now, with Xcode 12, you can distribute your Swift Packages as XCFramework binaries through Swift Package Manager.
This provides numerous advantages to you and the consumer. It allows you to keep your source code private and doesn't restrict your package repository to the structure imposed by Apple. For consumers, importing large packages in a binary format is significantly faster compared to traditional Swift Package, which are built as a library or framework.
To distribute your XCFramework using Swift Package Manager, you'll need to use a new target type: binaryTarget. Inside this target, provide values for the following keys:
- "name:" The name of the package. This name needs to match the module name of the XCFramework that you are distributing.
- "url:" The remote url to the zipped XCFramework.
- "checksum:" A checksum that validates the zipped XCFramework.
To compute the checksum, first ensure your command line is using // swift-tools-version:5.3
or later (Xcode tools 12.0) and confirm that the Package.swift file has been created. Then, navigate to the root of the package and run:swift package compute-checksum path/to/your/zipped/xcframework.zip
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "OneSignal",
products: [
.library(
name: "OneSignal",
targets: ["OneSignal"]),
],
targets: [
.binaryTarget(
name: "OneSignal",
url: "iOS_SDK/OneSignalSDK/OneSigna_XCFramework/OneSignal.zip",
checksum: "d9cf4274437a127108f6ec8f9bd424b1feb4f54dd240c2804d0a5bd31e068a70")
]
)
Supporting Binary Package Distribution While Using Xcode 11
Distributing a Swift Package as a binary requires package consumers to be using Swift tools 5.3, which is only available in Xcode 12. If you are still using Xcode 11, you won't be able to import your package — even if you have Xcode 12 installed and have set the command line tools to the Xcode 12 version. In order to support binary package distribution and Xcode 11, the SDK will need two Git repositories.
The original repository can store all of the source code, and the second repository will just contain a new Package.swift file that points to the zipped XCFramework file. I recommend attaching this zipped file as an asset to the associated GitHub release.
Implementing this strategy means that you must update Package.swift with the XCFramework’s new hash and update the link URL with the release version number for every new release. This can be done manually or can be automated using a bash script.
Updating Package.Swift and URL for New Releases Using Bash Script
- First, remove the previous archive and create a new one using the ditto command.
# Remove the old Zipped XCFramework and create a new Zip
echo "Removing old Zipped XCFramework ${FRAMEWORK_ZIP_PATH}"
rm -rf "${FRAMEWORK_ZIP_PATH}"
echo "Creating new Zipped XCFramework ${FRAMEWORK_ZIP_PATH}"
ditto -c -k --sequesterRsrc --keepParent "${FRAMEWORK_PATH}" "${FRAMEWORK_ZIP_PATH}"
2. Compute the new checksum.
echo "Computing package checksum and updating Package.swift ${SWIFT_PACKAGE_PATH}"
CHECKSUM=$(swift package compute-checksum "${FRAMEWORK_ZIP_PATH}")
3. Replace the old checksum line with the new one using a replacement command such as the StreamEDitor, sed.
SWIFT_PM_CHECKSUM_LINE=" checksum: \"${CHECKSUM}\""
# Use sed to remove line 17 from the Swift.package and replace it with the new checksum
sed -i '' "17s/.*/$SWIFT_PM_CHECKSUM_LINE/" "${SWIFT_PACKAGE_PATH}"
4. Update the URL to the zipped XCFramework with the latest release number using read and sed.
#Ask for the new release version number to be placed in the package URL
echo -e "\033[1mEnter the new SDK release version number\033[0m"
read VERSION_NUMBER
SWIFT_PM_URL_LINE=" url: \"https:\/\/github.com\/OneSignal\/OneSignal-iOS-SDK\/releases\/download\/${VERSION_NUMBER}\/OneSignal.xcframework.zip\","
# Use sed to remove line 16 from the Swift.package and replace it with the new URL for the new release
sed -i '' "16s/.*/$SWIFT_PM_URL_LINE/" "${SWIFT_PACKAGE_PATH}"
That’s it! You can now use Swift Package Manager to distribute a closed source binary that supports both Apple Silicon and Intel macs. Since Apple announced that they would begin transitioning from Intel chips to Apple silicon chips at WWDC 2020, SDK developers have been quick to make the switch — and the M1 chipset has helped facilitate the transition. Cocoapods, Carthage, and SwiftPM now all support XCFrameworks and are currently the best solutions to build a robust, universal SDK for Apple Developers.