Shipping Code to the World: A Guide to iOS & macOS Library Distribution
ios mobile library

Shipping Code to the World: A Guide to iOS & macOS Library Distribution

D. Rout

D. Rout

March 15, 2026 10 min read

On this page

You've built something brilliant. A networking layer so clean it sparks joy. A custom UI component your whole team loves. A utility so useful, strangers on the internet would pay for it. Now what?

Getting reusable code into other projects — and eventually into the hands of other developers — is a journey that, in Apple's ecosystem, has more than one well-worn path. Each path has its own philosophy, trade-offs, and loyal fanbase. Let's walk them all.


Why Distribution Frameworks Matter

Before diving in, it's worth asking: why do we even need dedicated tooling for this?

The short answer is dependency management is hard. You need a way to:

  • Declare what you depend on (and which version)
  • Resolve conflicts when two libraries need different versions of the same dependency
  • Integrate compiled or source code into an Xcode project without doing it manually every time
  • Update or remove dependencies without breaking everything

Apple's ecosystem — with its mix of Objective-C, Swift, frameworks, static libraries, and dynamically-linked binaries — has historically made this especially interesting. The tools below exist precisely because the problem is non-trivial.


CocoaPods — The OG Package Manager

Born: 2011 | Language: Ruby | Status: Mature, widely used, slowing in growth

If you've been writing iOS apps for any meaningful length of time, you've almost certainly encountered a Podfile. CocoaPods was the first widely adopted dependency manager for the Apple ecosystem, and for years it was simply the way things were done.

How It Works

CocoaPods operates via a centralised specification repository called the Specs Repo — essentially a giant index of versioned library metadata hosted on GitHub. When you run pod install, CocoaPods:

  1. Reads your Podfile to understand what you need
  2. Resolves the dependency graph
  3. Downloads source code or pre-built binaries
  4. Modifies your Xcode project to integrate everything

That last point is both its superpower and its most controversial design decision. CocoaPods doesn't ask permission — it rewrites your .xcodeproj file. This is why you work with .xcworkspace after installing pods, and why CocoaPods is often blamed for mysterious Xcode conflicts.

Publishing a Pod

To distribute your own library via CocoaPods, you write a .podspec file — a Ruby DSL that describes your library:

Pod::Spec.new do |s|
  s.name         = "MyAwesomeLibrary"
  s.version      = "1.0.0"
  s.summary      = "Does something genuinely useful."
  s.source       = { :git => "https://github.com/you/MyAwesomeLibrary.git", :tag => s.version }
  s.source_files = "Sources/**/*.swift"
end

Then pod trunk push uploads it to the central registry. Anyone can then add pod 'MyAwesomeLibrary' to their Podfile and pull it in.

The Trade-offs

CocoaPods is battle-tested, has massive ecosystem coverage, and supports some advanced features like subspecs (shipping optional parts of a library). But it requires Ruby, modifies your Xcode project in opaque ways, and has been slow to adopt modern Swift features. Many teams are migrating away — though for pure Objective-C legacy codebases, it often remains the pragmatic choice.

📖 Further reading:


Swift Package Manager (SPM) — Apple's First-Party Answer

Born: 2015 (Swift 3 era) | Language: Swift | Status: Rapidly becoming the standard

When Apple introduced Swift Package Manager, the community's reaction was somewhere between cautious optimism and "finally." An official, first-party solution with deep Xcode integration and no Ruby in sight.

SPM's design philosophy is deliberately simpler than its predecessors. There's no central registry — packages are identified by their Git URL, and versioning follows semantic versioning via tags.

The Package Manifest

Every Swift package is defined by a Package.swift file at its root:

// swift-tools-version:5.9
import PackageDescription
 
let package = Package(
    name: "MyAwesomeLibrary",
    platforms: [.iOS(.v16), .macOS(.v13)],
    products: [
        .library(name: "MyAwesomeLibrary", targets: ["MyAwesomeLibrary"]),
    ],
    targets: [
        .target(name: "MyAwesomeLibrary", path: "Sources"),
        .testTarget(name: "MyAwesomeLibraryTests", dependencies: ["MyAwesomeLibrary"]),
    ]
)

This single file declares your library's products, targets, dependencies, and minimum platform requirements. It's Swift all the way down.

Distribution Without a Registry

This is the conceptual leap that trips people up at first: you don't "publish" to SPM. You tag a release on GitHub (or any Git remote), and consumers point directly at that URL. There's no pod trunk push, no npm publish. The repository is the package.

https://github.com/you/MyAwesomeLibrary → tag: 1.0.0

In Xcode, File → Add Package Dependencies → paste the URL → done. That's genuinely the whole workflow.

Why SPM Is Winning

  • Zero tooling overhead — it's built into Swift and Xcode
  • Clean integration — no .xcworkspace gymnastics
  • Swift-native — understands the language's module system natively
  • Cross-platform — SPM packages can also target Linux and Windows

The main limitation has historically been binary distribution (more on that shortly) and support for complex Xcode project customizations, but both areas have improved substantially in recent years.

📖 Further reading:


Carthage — The Minimal Integrator

Born: 2014 | Language: Swift | Status: Stable, niche, but still used

Carthage emerged as a direct philosophical counterpoint to CocoaPods. Its founding principle: we will build your dependencies, hand you the frameworks, and then get out of the way. No modifying your Xcode project. No central repository. You do the integration.

The Carthage Workflow

A Cartfile lists your dependencies:

github "Alamofire/Alamofire" ~> 5.0
github "onevcat/Kingfisher" >= 7.0

Running carthage update fetches, builds, and produces .framework or .xcframework bundles in a Carthage/Build directory. From there, you drag them into your Xcode project. Manually. On purpose.

This is not laziness in the tooling. It's a philosophy: the framework respects that your Xcode project is yours, and that developers are capable of understanding what gets linked into their app.

Pre-built Binaries

Carthage also supports binary-only distribution via a binary.json specification — useful when you want to ship compiled frameworks without exposing source code (for commercial SDKs, for instance).

{
  "1.0.0": "https://example.com/MyLibrary-1.0.0.zip"
}

Who Still Uses Carthage?

Carthage has lost ground to SPM for most greenfield projects. But it retains a loyal following in teams that:

  • Prefer explicit, auditable integration steps
  • Need to distribute closed-source binary frameworks
  • Work in large modular monorepos with complex dependency graphs
  • Are allergic to magic

If your team does code review on every line of configuration, Carthage's transparency is genuinely appealing.

📖 Further reading:


XCFramework — The Binary Distribution Standard

Born: 2019 (Xcode 11) | Type: Bundle format, not a package manager

XCFramework deserves its own mention because it's not a package manager — it's a container format for distributing pre-compiled libraries that work across multiple platforms and architectures.

Before XCFrameworks, distributing a compiled library meant shipping fat binaries (.framework bundles containing slices for multiple architectures, stitched together with lipo). This became untenable when Apple Silicon Macs arrived: both iOS Simulator and macOS arm64 builds share the same CPU architecture but are different platforms, so lipo can't merge them.

What XCFramework Solves

An .xcframework bundle is essentially a zip of multiple .framework or .a (static library) directories, each labelled for a specific platform and architecture:

MyLib.xcframework/
├── ios-arm64/
│   └── MyLib.framework
├── ios-arm64_x86_64-simulator/
│   └── MyLib.framework
└── macos-arm64_x86_64/
    └── MyLib.framework

Xcode automatically picks the right slice for the build target. You can now ship one bundle that works on real devices, Simulators, and Macs — without any architecture conflicts.

Building an XCFramework

xcodebuild archive \
  -scheme MyLib \
  -destination "generic/platform=iOS" \
  -archivePath ./archives/iOS
 
xcodebuild archive \
  -scheme MyLib \
  -destination "generic/platform=iOS Simulator" \
  -archivePath ./archives/iOSSimulator
 
xcodebuild -create-xcframework \
  -framework ./archives/iOS.xcarchive/Products/Library/Frameworks/MyLib.framework \
  -framework ./archives/iOSSimulator.xcarchive/Products/Library/Frameworks/MyLib.framework \
  -output ./MyLib.xcframework

XCFrameworks integrate cleanly with both SPM (as binary targets) and Carthage. They're the preferred way to ship closed-source SDKs in the modern Apple ecosystem.

📖 Further reading:


Dynamic vs. Static Libraries: The Hidden Dimension

Every distribution approach forces a choice that has real performance implications: dynamic vs. static linking.

Static Library / Framework Dynamic Framework
Linked at Compile time Runtime
App launch Faster (no runtime resolution) Slower with many dylibs
Binary size Code embedded in app binary Shared, not duplicated
Memory Each target gets its own copy Single copy in memory
App Extensions Can cause duplication Shared safely

Apple's WWDC sessions have repeatedly flagged too many dynamic frameworks as a significant app launch time contributor. If you're building a framework for distribution, think carefully about whether it needs to be dynamic. For most utility libraries, static is the better default.

SPM defaults to static linking for library targets. CocoaPods defaults to static since version 1.9. Carthage builds dynamic frameworks by default but can be configured otherwise.

📖 Further reading:


How to Choose in 2025

Here's an honest framework for making the decision:

Choose Swift Package Manager if:

  • You're starting something new
  • Your library is written in Swift
  • You want zero extra tooling for consumers
  • You're targeting multiple Apple platforms

Choose CocoaPods if:

  • You need to support a very wide range of consumer projects (it still has the broadest ecosystem coverage)
  • Your library has complex subspec requirements
  • You're working heavily with Objective-C

Choose Carthage if:

  • You value explicit, auditable dependency integration
  • Your organisation has philosophical objections to CocoaPods' project modification approach
  • You're distributing a closed-source binary and want fine-grained control

Use XCFramework (in addition to the above) if:

  • You're distributing pre-compiled binaries
  • You need to support multiple platforms and architectures from one artifact
  • You're building a commercial SDK

The Bigger Picture: Modularisation

Library distribution isn't just for open-source projects. Many large iOS teams use these same tools for internal modularisation — splitting a monolithic app into a graph of independently compiled modules. The benefits: faster incremental build times, clearer ownership boundaries, enforced separation of concerns.

When a team reaches the point where a clean build takes 20 minutes, dependency frameworks become an architecture tool, not just a sharing mechanism. Understanding how they work under the hood — how linking works, what gets compiled when, how Xcode resolves modules — becomes genuinely essential knowledge.

📖 Further reading:


Closing Thoughts

The story of iOS/macOS library distribution is really a story about community building infrastructure before Apple did. CocoaPods solved a real, painful problem in 2011, and the community rallied around it. Carthage arrived with a principled alternative. SPM finally brought Apple's own answer — and it's genuinely good.

Today, Swift Package Manager is where the ecosystem is heading, and for good reason. But the others haven't disappeared because they solve real problems in real projects. Understanding all of them makes you a more capable, more confident Apple platform developer.

Now go publish something.


Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!