How do Swift Package Manager, CocoaPods, Carthage and XCFrameworks compare, and how do you choose?

16.2.1 · L3 · Interview

Predict first

A project depends on a small utility only through branch: "main". What is the main risk?

// A: nothing, the branch is always the newest and best version
// B: builds are not reproducible, because main can change or break between builds
// C: it cannot be resolved
  • A
  • B
  • C

Mental model. SPM is the supermarket on the corner everyone uses: shelves labelled by version and a receipt (the lockfile). CocoaPods is the big warehouse club that changes your kitchen layout, Carthage buys finished meals, and an XCFramework is the standard-size box that finished meals come in.

Swift Package Manager (SPM) is Apple’s built-in tool and the default today: dependencies are declared in Package.swift (or added in Xcode), resolved from Git tags using semantic-version rules (from: "1.2.0" means up to the next major, exact, a range, a branch or a revision), pinned in Package.resolved, and built from source by the same build system as the app. It supports source and binary targets, resources, plugins and macros, and works with Xcode, swift build and CI. CocoaPods is the older, Ruby-based manager with a central spec repository: a Podfile produces a workspace that integrates the pods as extra projects, and Podfile.lock pins versions. It has the biggest historical library catalogue, supports Objective-C and mixed-language pods and custom build phases, but modifies your Xcode project, is slower to integrate and its central spec repository is being made read-only, so new libraries increasingly publish only to SPM. Carthage is decentralised: it builds your dependencies into prebuilt frameworks that you link yourself, so it does not touch your project and builds are fast once cached, but it needs each dependency to build as a framework (more manual setup, slower to maintain) and has little new activity. An XCFramework is not a manager but a packaging format: a bundle holding the same library built for several platforms and architectures (device, simulator, macOS) with module information, used for binary distribution by any of the managers.

Choosing: use SPM unless something prevents it. Reasons to keep CocoaPods: an existing large Podfile, a dependency that is only available there or uses custom build phases, or a mixed Objective-C code base with pods that are not yet SPM-compatible. Reasons for prebuilt binaries (XCFrameworks, .binaryTarget): faster builds for large stable dependencies, distributing an SDK without source, or supplying a vendor’s closed code. Whatever you pick: pin versions and commit the lockfile so everyone and CI build the same thing; use version ranges deliberately (a floating branch: "main" is not reproducible); vet each dependency’s maintenance, licence, size, transitive dependencies and privacy manifest; avoid depending on many tiny libraries for trivial code; wrap third-party APIs behind your own interface so a replacement stays cheap (see the architecture topic); and watch for duplicate copies of the same dependency pulled in by different parents. Supply-chain care matters: a dependency is code running inside your app with your app’s permissions.

A moving branch changes under you. Use a version requirement so the lockfile pins a known revision.

Source of truthIntegrationDelivers
SPMPackage.swift, Package.resolvedbuilt into Xcode and the toolchainsource or binary targets
CocoaPodsPodfile, Podfile.lockgenerates a workspace and extra projectssource or vendored binaries
CarthageCartfile, Cartfile.resolvedyou link prebuilt frameworks yourselfprebuilt frameworks
XCFrameworka packaging format, not a manageradded by any manager or by handbinary for several platforms and architectures

Common traps

  • Depending on a branch (main) without a pinned revision. Builds change underneath you; use version rules and commit Package.resolved.
  • Adding a dependency for a few lines of code. You inherit its bugs, size, privacy obligations and its own dependencies.

They might follow up with

  • What does from: "1.2.0" allow? Any version from 1.2.0 up to, but not including, 2.0.0.
  • When do you prefer a binary dependency? To save build time on large stable code, or to distribute without source.

Related questions

  • 16.2.2 What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a…
  • 16.1.1 What are targets, products and modules in a Swift package, and what does a first Package.swift look like?
  • 14.3.1 What is a privacy manifest, what are required-reason APIs, and what must third-party SDKs declare?

What is an XCFramework, what do module maps and library evolution… →