What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a binary target?

16.2.2 · L5 · Interview

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 arm64 but 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…