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

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:
- Reads your
Podfileto understand what you need - Resolves the dependency graph
- Downloads source code or pre-built binaries
- 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
.xcworkspacegymnastics - 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:
- Swift Package Manager Documentation — Swift.org
- Creating a Standalone Swift Package — Apple Developer
- Swift Package Index — Community Package Registry
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:
- Distributing Binary Frameworks as Swift Packages — Apple Developer
- Binary Frameworks in Swift — WWDC 2019 Session 416
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:
- The State of iOS Modularization at Spotify — Spotify Engineering Blog
- Tuist Blog — Tooling for large-scale Xcode projects
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.
Read next
Comments (0)
Join the conversation
Sign in to leave a comment on this post.
No comments yet. to be the first!