All about xcframeworks
You may have heard about xcframework
s 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 xcframework
s and put it all in one place for your reference.
This section provides a rationale for the existence of xcframework
s. Feel free to skip it if you’re not interested.
Before xcframework
s 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
- 📂 dSYMs
- 📁 ios-arm64_x86_64-simulator
- 📂 dSYMs
- 📚 MyFramework.framework.dSYM
- 📚 MyFramework.framework
- 📂 dSYMs
- 📁 tvos-arm64
- 📂 dSYMs
- 📚 MyFramework.framework.dSYM
- 📚 MyFramework.framework
- 📂 dSYMs
- 📁 tvos-arm64_x86_64-simulator
- 📂 dSYMs
- 📚 MyFramework.framework.dSYM
- 📚 MyFramework.framework
- 📂 dSYMs
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
- 📂 dSYMs
- 📁 ios-arm64_x86_64-simulator
- 📂 dSYMs
- 📚 libMyLib.a.dSYM
- 📂 Headers
- 📝 MyLib.h
- 📝 libMyLib.a
- 📂 dSYMs
- 📁 macos-arm64_x86_64
- 📂 dSYMs
- 📚 libMyLib.a.dSYM
- 📂 Headers
- 📝 MyLib.h
- 📝 libMyLib.a
- 📂 dSYMs
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 |
macosx 1 |
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
- 📂 Frameworks
- 📂 Library
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
- 📂 lib
- 📂 local
- 📂 usr
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
).
Assemble related xcarchive
s into an xcframework
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 xcframework
s from built products
It is possible to create xcframework
s 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 xcframework
s in your Xcode project
Using xcframework
s 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 xcframework
s are, how to create them, and how to use them in your projects. There is more to say about how to use xcframework
s 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 xcframework
s 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
.
-
Building for Mac Catalyst also sets
IS_MACCATALYST
toYES
. ↩