What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a binary target?
Predict first
A team wants to ship an SDK as a binary Swift framework that customers on newer compilers can import. What must it be built with?
// A: nothing special, binaries work with any compiler
// B: library evolution mode, producing module interface (.swiftinterface) files
// C: only Objective-C code
- A
- B
- C
Mental model. An XCFramework is a luggage set with a case for every kind of trip: the airline (Xcode) takes the right case for the destination. Library evolution is labelling the contents in a universal language so a newer airline can still unpack it.
A framework (or static library) built for one platform and architecture cannot run elsewhere: the device (arm64) and the simulator (arm64 and x86_64, a different platform slice) conflict if merged in one “fat” binary. An XCFramework solves this: a directory holding one slice per platform-and-variant (iOS device, iOS simulator, macOS, Catalyst…) plus an Info.plist describing them, and Xcode picks the right slice for the build. You create one with xcodebuild -create-xcframework -framework … -output X.xcframework, after archiving the framework for each destination. Each slice carries the module’s interface so Swift clients can import it: for Swift code, .swiftmodule files, and for a stable one, module interface files (.swiftinterface), which are textual and survive compiler upgrades; for C and Objective-C code, headers and a module map (module.modulemap) declaring which headers form the Clang module and what it links.
The Swift catch is compiler and ABI compatibility. A binary .swiftmodule is tied to the exact compiler version that built it, so a binary Swift framework needs library evolution mode (-enable-library-evolution, BUILD_LIBRARY_FOR_DISTRIBUTION = YES), which produces the .swiftinterface files that any later compiler can read and keeps layout choices resilient (see the library-evolution topic for @frozen and @inlinable). It costs some performance and constrains API changes. Using one in SPM is a .binaryTarget(name:url:checksum:) (a remote zip with a checksum, so builds verify what they download) or .binaryTarget(name:path:) (a local XCFramework); the package manager copies the slice into the build. When to use binaries: you ship an SDK to customers and cannot share source; a stable internal module is large and slows every clean build; or a vendor supplies closed code. Costs: debugging without source (ship dSYMs and, if you can, debug-friendly variants), update friction, larger repositories or downloads, Objective-C and Swift interplay through module maps, duplicate symbols if the same code arrives both as source and as a binary, and trust in a prebuilt artefact (verify the checksum and signature; Apple now expects privacy manifests and signatures on commonly used SDK binaries). Prefer source dependencies by default, and make a binary only when there is a clear payoff.
A plain Swift binary module is tied to one compiler. Library evolution produces textual interfaces and stable layouts that later compilers can read.
Common traps
- Shipping a binary Swift framework without library evolution. It only works with the exact compiler that built it, so clients on another compiler fail to import it.
- Merging device and simulator slices into one fat library. Both are
arm64but different platforms; use an XCFramework.
They might follow up with
- Why does a binary target need a checksum? So the build verifies the downloaded artefact is the one you intended.
- What does a module map do? Declares which C or Objective-C headers form a Clang module that Swift can import.
Related questions
- 16.2.1 How do Swift Package Manager, CocoaPods, Carthage and XCFrameworks compare, and how do you choose?
- 16.3.1 How do static and dynamic linking differ for an app, what do they cost at launch and in size, and what are…
- 16.1.2 How do internal, package and public work between modules, and what are the classic surprises?
← How do Swift Package Manager, CocoaPods, Carthage and XCFrameworks…