There's a moment every iOS developer eventually hits. You're starting a new project, and you find yourself opening an old one — not for inspiration, but to copy a file. Maybe it's that networking layer you've refined over months, or a suite of Date extensions you can't live without, or a custom logging utility that took you three iterations to get right.
You paste it in, maybe tweak the module name, and move on. It works. For now.
Then the bug appears.
You fix it in Project A. Six months later you find the same bug in Project B and C, because they're still running the original copy. If this sounds familiar, you've hit the inflection point where Swift Package Manager (SPM) stops being a "nice to have" and starts being a necessity.
The Problem with Duplication
Code duplication across projects isn't just an aesthetic problem — it's a maintenance liability. Every divergence between copies is a potential bug. Every fix you make in one place is a fix you might forget to make in three others.
The common workarounds each come with their own costs:
- Copy-paste — fast at first, painful later
- Git submodules — technically sound, but notoriously awkward to work with
- Shared workspace — tightly couples projects in ways that don't scale
- Copying files into a shared framework target — gets messy fast when you need different subsets in different apps
Swift Package Manager solves this cleanly. It's Apple's first-party dependency system, integrated directly into Xcode, and it's the right tool for sharing your own code between projects.
What Makes a Good Candidate for a Package?
Before you start extracting code, it's worth asking: what actually belongs in a package?
Good candidates tend to be:
- Self-contained utilities — extensions, helpers, formatters
- Networking layers — a generic API client, retry logic, request builders
- UI components — custom views and controls you reuse across apps
- Domain logic — business rules that multiple apps share (e.g., a shared data model for a suite of apps)
Bad candidates are things tightly coupled to a single app's architecture, or code that changes so frequently you'd be cutting new package versions every week.
Start with the thing you've pasted more than twice. That's your first package.
Creating Your First Local Package
Let's say you've got a collection of Date utilities you keep dragging between projects. Here's how to extract it.
1. Create the package
In Terminal:
mkdir DateKit
cd DateKit
swift package init --name DateKit --type library
This scaffolds a standard package structure:
DateKit/
├── Package.swift
├── Sources/
│ └── DateKit/
│ └── DateKit.swift
└── Tests/
└── DateKitTests/
2. Write your Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "DateKit",
platforms: [.iOS(.v16), .macOS(.v13)],
products: [
.library(name: "DateKit", targets: ["DateKit"]),
],
targets: [
.target(name: "DateKit"),
.testTarget(name: "DateKitTests", dependencies: ["DateKit"]),
]
)
3. Move your code in
Drop your extensions and utilities into Sources/DateKit/. For example:
// Sources/DateKit/Date+Formatting.swift
import Foundation
public extension Date {
func formatted(as style: DateFormat) -> String {
let formatter = DateFormatter()
formatter.dateFormat = style.rawValue
return formatter.string(from: self)
}
var isToday: Bool {
Calendar.current.isDateInToday(self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
}
public enum DateFormat: String {
case short = "MM/dd/yy"
case medium = "MMM d, yyyy"
case iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ"
}
Note the public access modifiers — everything you want consumers to use needs to be explicitly public. This is easy to forget coming from app targets where internal access is usually sufficient.
Adding the Package to Your Projects
Local (during development)
While you're iterating on the package, use a local reference in Xcode:
File → Add Package Dependencies → Add Local… then select the package folder.
Or in another package's Package.swift:
dependencies: [
.package(path: "../DateKit"),
],
This is great during active development — changes to the package are reflected immediately without needing to publish a version.
Structuring for Multiple Products
As your package grows, you might want to expose multiple libraries from a single package — useful when some consumers only need a subset of functionality.
let package = Package(
name: "CoreKit",
products: [
.library(name: "NetworkingKit", targets: ["NetworkingKit"]),
.library(name: "DateKit", targets: ["DateKit"]),
.library(name: "UIComponents", targets: ["UIComponents"]),
],
targets: [
.target(name: "NetworkingKit"),
.target(name: "DateKit"),
.target(name: "UIComponents", dependencies: ["DateKit"]),
]
)
Consumers can import exactly what they need. An app that only wants DateKit doesn't have to pull in UIComponents.
A Note on Testing
One of the underrated benefits of extracting code into a package is that it forces testability. When your utilities live in an app target, they're often entangled with UIKit, app state, or things that are hard to test in isolation.
A package has no such dependencies unless you explicitly add them. This is a feature. Write tests in Tests/DateKitTests/ and run them with:
swift test
Or directly from Xcode. Clean, fast, no simulator required.
The Payoff
The upfront cost of creating a package is real — maybe an hour the first time. But every subsequent project that imports it instead of copy-pasting pays that back quickly. You fix a bug once. You add a feature once. You test it once.
The moment you find yourself opening an old project to borrow code, that's your sign. Extract it, give it a proper home, and let SPM do the rest.
Have questions about structuring packages for larger teams, or working with packages that have their own dependencies? Happy to go deeper on any of it.