humancode.us

All about xcframeworks

May 19, 2023

You may have heard about xcframeworks and how they are a way to distribute libraries on Apple platforms. But documentation on the format is hard to come by. With this blog post I hope to summarize everything you need to know about xcframeworks and put it all in one place for your reference.

This section provides a rationale for the existence of xcframeworks. Feel free to skip it if you’re not interested.

Before xcframeworks were defined, there was no Apple-supported way to distribute libraries for more than one platform at a time. This wasn’t really a problem for a while, since the only Apple platform a developer could build a framework for was OS X. Even when the Mac switched from PowerPC to Intel, the single-framework model survived because it was possible to create Universal “fat” binaries that contained more than one architecture slice. Since a framework would still target a single platform, including a Universal binary worked quite well.

When Xcode began supporting frameworks for the iPhone SDK, the picture became more complicated, since developers had to deal with two new platforms: iPhoneOS and iPhone Simulator. Developers could ship two frameworks—one for each platform—but it was not easy to consume two distinct frameworks in a project that should be easily built for both iPhone hardware and the simulator. While build-settings tricks could be employed to switch FRAMEWORK_SEARCH_PATHS depending on the build platform, the lack of automatic support in Xcode for doing so made it a difficult chore.

One workaround that some developers adopted was to use the lipo tool to splice an arm64 slice from the iOS build of the framework, together with an x86_64 slice from the Simulator build, and distribute a single “Frankenframework” that could be used to build both a device binary and a Simulator binary, without build-settings shenanigans. This hack conflates architecture with platform but it worked…

…until the M1 Mac came along. Since the M1 Mac natively used the arm64 architecture, both iPhone hardware and Simulator builds consumed the arm64 architecture; developers could no longer use architecture as a proxy for platform. Add to that the proliferation of platforms—tvOS and watchOS and their simulators—and it was clear that a better solution was needed.

Apple defined the xcframework format as a way to cut through this chaos. This officially-supported specification allowed developers to distribute the same library built for a variety of platforms and architectures in a single package. Projects that consumed an xcframework needed only specify a single link dependency, and Xcode would switch between platforms and architectures automatically. The xcframework specification is future-proofed enough to accommodate new platforms or architectures Apple might support in the future.

An xcframework is a library distribution bundle

To be precise, an xcframework is a universal, binary, library distribution format. Let’s break that description down in reverse order.

An xcframework is a library distribution format. Each xcframework holds exactly one library. A library is a precompiled collection of code that can be consumed by another project to create an executable (or app).

An xcframework is a binary distribution format. That means it does not include source code in its distribution. Only built binaries and interface specifications (headers and/or Swift interface files) are included.

An xcframework is a universal distribution format. That means it holds libraries and interfaces for different platforms as well as processor architectures in the same structure. A single xcframework can, for example, offer the same library for consumption for iOS, watchOS, and Mac projects using either Intel or ARM architectures.

Finally, an xcframework is a bundle because it’s a directory with a well-defined content structure and file extension, and has an Info.plist file in its root. Examining its Info.plist file shows that it has a CFBundlePackageType of XFWK.

An xcframework can hold either a framework, or a library plus headers

An xcframework can be created to hold either a framework built for various platforms and architectures, or a plain library (.a file) built for various platforms and architectures plus its headers.

An xcframework that contains a framework can hold everything that a framework can, including a module definition, headers, Swift interface files, resources, and localizations.

Although Swift files can be compiled into plain libraries, I don’t know the correct way to include the swiftinterface files that must accompany it without actually creating a framework. If you know, please send me an email and tell me how to do it right.

In either case, you have the option to build your framework or plain library as either static or a dynamic to package them in an xcframework.

An xcframework uses subdirectories to organize variants by platform

An xcframework uses subdirectories to organize variants of your library by platform. The best way to discover the directory names for each variant is by parsing the Info.plist file. The AvailableLibraries key in the top-level dictionary contains an array of dictionaries, each corresponding to one variant.

Here’s an example of one variant’s entry in an xcframework containing a framework:

<dict>
    <key>DebugSymbolsPath</key>
    <string>dSYMs</string>
    <key>LibraryIdentifier</key>
    <string>ios-arm64_x86_64-maccatalyst</string>
    <key>LibraryPath</key>
    <string>MyFramework.framework</string>
    <key>SupportedArchitectures</key>
    <array>
        <string>arm64</string>
        <string>x86_64</string>
    </array>
    <key>SupportedPlatform</key>
    <string>ios</string>
    <key>SupportedPlatformVariant</key>
    <string>maccatalyst</string>
</dict>

This entry shows where you can find the entry for the MyFramework.framework library, built for the ios platform with variant maccatalyst (i.e. a Mac Catalyst library). The LibraryIdentifier gives you the directory name under which you can find the framework. The SupportedArchitectures key tells you what CPU architectures the library supports. In this case, MyFramework.framework contains a Universal (fat) binary that contains arm64 and x86_64 architecture slices.

For a plain framework, a similar entry may look like the following:

<dict>
    <key>DebugSymbolsPath</key>
    <string>dSYMs</string>
    <key>HeadersPath</key>
    <string>Headers</string>
    <key>LibraryIdentifier</key>
    <string>ios-arm64_x86_64-maccatalyst</string>
    <key>LibraryPath</key>
    <string>libStaticLib.a</string>
    <key>SupportedArchitectures</key>
    <array>
        <string>arm64</string>
        <string>x86_64</string>
    </array>
    <key>SupportedPlatform</key>
    <string>ios</string>
    <key>SupportedPlatformVariant</key>
    <string>maccatalyst</string>
</dict>

Notice the presence of a HeadersPath key.

Here is the directory layout of a typical xcframework containing a ‘framework’. It contains versions built for iOS, iOS Simulator, tvOS, and tvOS Simulator:

  • 📂 MyFramework.xcframework
    • 📝 Info.plist
    • 📂 ios-arm64
      • 📂 dSYMs
        • 📚 MyFramework.framework.dSYM
      • 📚 MyFramework.framework
    • 📁 ios-arm64_x86_64-simulator
      • 📂 dSYMs
        • 📚 MyFramework.framework.dSYM
      • 📚 MyFramework.framework
    • 📁 tvos-arm64
      • 📂 dSYMs
        • 📚 MyFramework.framework.dSYM
      • 📚 MyFramework.framework
    • 📁 tvos-arm64_x86_64-simulator
      • 📂 dSYMs
        • 📚 MyFramework.framework.dSYM
      • 📚 MyFramework.framework

And here’s one that contains a plain library. It contains versions built for iOS, iOS Simulator, and macOS.

  • 📂 MyLib.xcframework
    • 📝 Info.plist
    • 📂 ios-arm64
      • 📂 dSYMs
        • 📚 libMyLib.a.dSYM
      • 📂 Headers
        • 📝 MyLib.h
      • 📝 libMyLib.a
    • 📁 ios-arm64_x86_64-simulator
      • 📂 dSYMs
        • 📚 libMyLib.a.dSYM
      • 📂 Headers
        • 📝 MyLib.h
      • 📝 libMyLib.a
    • 📁 macos-arm64_x86_64
      • 📂 dSYMs
        • 📚 libMyLib.a.dSYM
      • 📂 Headers
        • 📝 MyLib.h
      • 📝 libMyLib.a

As you can see, the top-level directories are named after the LibraryIdentifier in the Info.plist. Under each top-level directory, a copy of the framework or plain built for that platform is included.

Although you should always use the Info.plist as your reference, the directory names follow the pattern platform-arch_arch-variant.

Unsurprisingly, the strings used in this pattern match neither the PLATFORM_NAME build setting in Xcode, nor the -destination parameter for Xcode’s build tools. Here’s a handy mapping between those symbols. The Prefix and Suffix columns indicate the strings used to create the subdirectory names.

Platform Prefix Suffix PLATFORM_NAME destination
iPhone ios   iphoneos generic/platform=ios
iPhone simulator ios simulator iphonesimulator generic/platform=ios Simulator
watchOS watchos   watchos generic/platform=watchos
watchOS simulator watchos simulator watchsimulator generic/platform=watchos Simulator
tvOS tvos   appletvos generic/platform=tvos
tvOS simulator tvos simulator appletvsimulator generic/platform=tvos Simulator
macOS macos   macosx generic/platform=macos
Mac Catalyst ios maccatalyst macosx1 generic/platform=macos,variant=maccatalyst

Construct an xcframework

Create one xcarchive per platform

The first step to creating an xcframework is to create one xcarchive of your build output for each platform that you intend to support.

To create an xcarchive, invoke the following command:

xcodebuild archive \
    -project 'path/to/MyFramework.xcodeproj' \
    -scheme 'MyFramework' \
    -configuration Release \
    -archivePath 'path/to/MyFramework.xcarchive' \
    -destination 'generic/platform=ios' \
    SKIP_INSTALL=NO \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES

Substitute your own project and scheme name, of course. The -destination parameter should be one of the destinations found in the table above.

The -archivePath parameter should point to a unique path name for your archive per platform, ending with an .xcarchive file extension.

The SKIP_INSTALL=NO and BUILD_LIBRARY_FOR_DISTRIBUTION=YES settings ensure that all installable artifacts and .swiftinterface files are emitted into the archive if needed.

The xcarchive format is itself a bundle. For framework archives, the directory structure is as follows:

  • 📂 MyFramework.xcarchive
    • 📝 Info.plist
    • 📂 dSYMs
      • 📚 MyFramework.framework.dSYM
    • 📂 Products
      • 📂 Library
        • 📂 Frameworks
          • 📚 MyFramework.framework

If you look in MyFramework.framework/Modules/MyFramework.swiftmodule, you’ll find a colection of Swift interface definitions, including .swiftinterface files that are generated thanks to the BUILD_LIBRARY_FOR_DISTRIBUTION setting.

For plain library archives, the directory structure is as follows:

  • 📂 MyLib.xcarchive
    • 📝 Info.plist
    • 📂 dSYMs
      • 📚 libMyLib.a.dSYM
    • 📂 Products
      • 📂 usr
        • 📂 local
          • 📂 lib
            • 📝 libMyLib.a
          • 📂 include
            • 📝 MyLib.h

Note that the directory structure under Products reflects the installation path specified for the library target in your project; this directory structure would be rooted in / if you had decided to install this library on your system. You can change where your library is installed using the INSTALL_PATH build setting.

The headers for your library are not installed by the default library target template. To install them, look for the Copy Files phase of your project which installs your headers, and change its destination to Absolute Path, giving it the absolute directory path of your headers (in this case /usr/local/include).

Once you have built an xcarchive for each platform you wish to support, it’s time to assemble them into a single xcframework.

To create an xcframework that contains a framework, use the following command:

xcodebuild -create-xcframework \
    -archive 'path/to/MyFramework.xcarchive'
    -framework 'MyFramework.framework' \
    ... \
    -output 'path/to/MyFramework.xcframework'

Add one pair of -archive and -framework parameters for each archive that you have built.

To create an xcframework that contains a plain library, use the following command:

xcodebuild -create-xcframework \
    -archive 'path/to/MyLib.xcarchive' \
    -library 'libMyLib.a' \
    -headers 'path/to/MyLib.xcarchive/Products/usr/include' \
    ... \
    -output 'path/to/MyLib.xcframework'

Add one set of -archive, -library, and -headers parameters for each archive that you have built.

Note that the -headers parameters takes a full path to the headers directory. This path could point into the archive (recommended) or into a directory in your source repository. All files in that directory will be copied into the Headers directory in the resulting xcframework.

If you have done this correctly, you will now have an xcframework that you can distribute. Congratulations! You can now .zip archive the .xcframework and distribute it to your heart’s content.

Alternative: Assemble xcframeworks from built products

It is possible to create xcframeworks from your built products by passing the path to a built .framework or library as the values of the -framework or -library parameters in the -create-xcframework command above. For most workflows, however, I recommend creating an archive as an intermediate step because that’s the workflow that Apple recommends.

Consume xcframeworks in your Xcode project

Using xcframeworks in your Xcode project involves adding them as link-time dependencies. In your consuming target’s Link Binary With Libraries build phase, add a dependency on the xcframework (you might have to select the “Add Files…” drop-down to find the bundle). You may elect to copy the xcframework into your project directory if you want to keep a copy in your source repository.

If you are creating an app, and need to include a dynamic framework within the app bundle, add the xcframework to the Embed Frameworks build phase of your app target. The vagaries of adding static frameworks are a subject for another blog post.

Epilogue

This concludes a whirlwind tour on what xcframeworks are, how to create them, and how to use them in your projects. There is more to say about how to use xcframeworks in your workflow, but I think there is enough here to get the job done for most use cases.

Note that I did not cover offering and consuming xcframeworks using SwiftPM, using its .binaryTarget() specifier. Suffice it to say that using SwiftPM in this way is rather awkward—you’d probably be better off consuming the xcframework directly rather than through SwiftPM in Xcode. Perhaps there is another blog post for that.

If you have comments, criticisms, or corrections to this blog post, I’d appreciate an email at dave at humancode dot us.

  1. Building for Mac Catalyst also sets IS_MACCATALYST to YES