Why XCFrameworks Matter

Now that Apple Silicon and Catalyst are out, library developers are going to want to start using XCFrameworks over Universal Frameworks in the immediate future. To explain why, let’s look at how building an iOS library has changed over the last few years.

The following code presumes the existence of an iOS Framework project called ExampleLibrary. If you’d like to follow along, the full source can be found on GitHub.


The year is 2018, and we’re going to build a library. At this point in history, library creation is made relatively simple with Universal Frameworks. Universal Frameworks are a container format that let us bundle together many single-architecture frameworks (slices) into one; users can drop that framework into Xcode and Xcode will handle selecting the correct architecture when building. So for our library, we’d build a Universal Framework that supports the architectures iOS devices do, meaning arm64, armv7, and so on. Let’s see what that looks like:

echo "Building for iOS..."
xcodebuild archive \
    -sdk iphoneos IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch armv7 -arch arm64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphoneos/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

This is a start; however, our users will almost certainly want our library to support the iOS Simulator, and this is where it gets tricky. Users can’t conditionally specify a different framework to use when compiling for the iOS Simulator. Unless we’re willing to give our users source code access, this means that our Universal Framework for iOS needs to also somehow support the iOS Simulator platform.

Though it is a hack (foreshadowing), the common way to solve this problem is to misuse the structure of Universal Frameworks a bit. Essentially, what we’re going to do is the following:

  1. Build a Universal Framework against the iOS SDK, targeting iOS architectures.
  2. Build a Universal Framework against the iOS Simulator SDK, targeting macOS architectures.
  3. Use lipo to combine the two frameworks into a single framework for distribution.

Here we’re taking advantage of the fact that only the slice for the current architecture will be used by a given platform. Because iOS runs on ARM and the iOS Simulator runs on x86_64, that means that we’ll never run the risk of using a slice built with the wrong SDK. As an example, let’s modify our prior code to generate our combined Universal Framework:

echo "Building for iOS..."
xcodebuild archive \
    -sdk iphoneos IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch armv7 -arch arm64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphoneos/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for iOS Simulator..."
xcodebuild archive \
    -sdk iphonesimulator IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch x86_64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphonesimulator/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building Framework with iOS and iOS Simulator support..."
lipo -create -output "./ExampleLibrary.framework" \
    "./build/iphoneos/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework/ExampleLibrary" \
    "./build/iphonesimulator/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework/ExampleLibrary"

We can use lipo in Terminal to see that our combined Universal Framework now has slices for iOS devices (armv7 and arm64), and macOS devices (x86_64):

$ lipo -info ./ExampleLibrary.framework
Architectures in the fat file: ./ExampleLibrary.framework are: armv7 x86_64 arm64

We can distribute this library and iOS users will be able to use it in the simulator without having to do anything special*. We can even distribute our library with CocoaPods or Carthage, which expect us to use this technique. All is well in the world.

*It should be noted here that doing this means that you’ll have to strip the Simulator slices from the framework before submitting your app to the App Store, or you’ll be rejected. CocoaPods and Carthage automate this process.


The year is 2019. macOS Catalina has released, and with it comes Catalyst. Catalyst allows developers to build their iOS apps for macOS using a special version of the iOS SDK. Our Universal Framework now needs to support macOS architectures and this new SDK.

We have a problem, though! It turns out that Universal Frameworks have a strict one-slice-per-architecture limit. Put differently, we can’t support the same architecture on multiple platforms in a single Universal Framework. Before, we got away with multi-platform support between iOS and the iOS Simulator because the hardware that ran them didn’t share any architectures. Catalyst breaks this hack since we would be using all the same architectures that the iOS Simulator would use. If we want to support iOS, the iOS Simulator, and Catalyst, we’ll need to figure something else out.

This is where XCFrameworks come into play. This is a new container format with a particular structure. Whereas Universal Frameworks contain many slices without knowledge of the platform SDK for each one, XCFrameworks contain many slices organized by platform. Because of this platform-awareness, an XCFramework can target more than one platform for the same architecture, which solves our problem.

To construct an XCFramework, we generate a single Universal Framework for each platform, each supporting whatever architectures the platform can be found on. Instead of using lipo to stitch them together as we did in 2018, though, we’re going to use a new command. Here’s our previous code with some tweaks to support this new method:

echo "Building for iOS..."
xcodebuild archive \
    -sdk iphoneos IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch armv7 -arch arm64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphoneos/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for iOS Simulator..."
xcodebuild archive \
    -sdk iphonesimulator IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch x86_64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphonesimulator/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for Catalyst..."
xcodebuild archive \
    -destination "generic/platform=macOS,variant=Mac Catalyst" MACOSX_DEPLOYMENT_TARGET=10.15 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/maccatalyst/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building XCFramework..."
xcodebuild -create-xcframework -output ./ExampleLibrary.xcframework \
    -framework "./build/iphoneos/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework" \
    -framework "./build/iphonesimulator/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework" \
    -framework "./build/maccatalyst/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework"

The output is ExampleLibrary.xcframework, which users can add to their projects just like they use any other framework. If we open it in Finder, we’ll see that we have separate frameworks inside grouped by platform rather than architecture, as promised:

A screenshot of the folder structure of the generated XCFramework. It contains frameworks grouped by platform rather than by architecture. One for iOS, one for Simulator, and one for Catalyst.

An important thing to note here is that we’re no longer misusing Universal Frameworks to get iOS Simulator support. It’s now a separate framework internally. This is great! We can now theoretically add support for arbitrary new platforms, too. That would be really useful if Apple did something big, like, I don’t know, announcing that they’re going to switch the Mac lineup from x86 to ARM.


The year is 2020. Apple has announced they’re going to switch the Mac lineup from x86 to ARM. Specifically, they’ve announced M1, an Apple Silicon processor for their new laptops and desktops. Because these are ARM processors, we can port your iOS ARM code to macOS ARM with very little effort.

If we were still using Universal Frameworks, we’d run into the same issue we had with Catalyst, as we’d be trying to build for arm64 for both iOS and macOS. Because we did the work to use an XCFramework instead, though, supporting M1 is as easy as generating a new framework and adding that to our bundle:

echo "Building for iOS..."
xcodebuild archive \
    -sdk iphoneos IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch armv7 -arch arm64 \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphoneos/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for iOS Simulator..."
xcodebuild archive \
    -sdk iphonesimulator IPHONEOS_DEPLOYMENT_TARGET=9.0 \
    -arch x86_64 -arch arm64 \ # Added arm64 arch for M1
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/iphonesimulator/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for Catalyst..."
xcodebuild archive \
    MACOSX_DEPLOYMENT_TARGET=10.15 \
    -destination "platform=macOS,variant=Mac Catalyst" \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/maccatalyst/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building for macOS..."
xcodebuild archive \
    -sdk macosx MACOSX_DEPLOYMENT_TARGET=11.0 \
    -arch x86_64 -arch arm64 \ # Added arm64 arch for M1
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
    -scheme "ExampleLibrary" \
    -archivePath "./build/macosx/ExampleLibrary.xcarchive" SKIP_INSTALL=NO

echo "Building XCFramework..."
xcodebuild -create-xcframework -output ./ExampleLibrary.xcframework \
    -framework "./build/iphoneos/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework" \
    -framework "./build/iphonesimulator/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework" \
    -framework "./build/maccatalyst/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework" \
    -framework "./build/macosx/ExampleLibrary.xcarchive/Products/Library/Frameworks/ExampleLibrary.framework"

We now have a single framework that supports macOS on both Intel and Apple Silicon processors, iOS, iOS Simulator, and Catalyst.

To sum it all up: if you’re a library developer, you should start using XCFramework as soon as possible, even if you’re only supporting iOS! This will let you support multiple platforms that share architectures, which is very important as Apple plans to unite all their devices under the ARM architecture.

Leave a Reply

Your email address will not be published. Required fields are marked *