Swift Package Manager — Creating, Publishing, and Managing Packages
In this tutorial, you will learn about Swift Package Manager. We cover key concepts, practical examples, and best practices to help you master this topic.
Swift Package Manager (SPM) is Apple's official build system and dependency manager integrated into Swift, Xcode, and server-side Swift frameworks like Vapor for distributing reusable libraries.
What You'll Learn
- Package.swift structure and configuration
- Creating and building Swift packages
- Adding and managing dependencies
- Publishing packages on GitHub
- Versioning with semantic versioning
- Using SPM in iOS and macOS apps
Why It Matters
SPM eliminates the need for third-party dependency managers like CocoaPods and Carthage for most projects. It is integrated into Xcode, supports binary frameworks, and is the standard way to distribute Swift libraries. The Swift community publishes thousands of packages on SPM.
Real-World Use
A team developing a social media app uses SPM to pull in Alamofire for networking, SwiftyJSON for JSON parsing, and Kingfisher for image Caching. They also maintain an internal "AppCore" package shared across multiple apps. Adding a new dependency is a single line in Package.swift.
Learning Path
flowchart LR A[Testing
Lesson 31] --> B[Swift Package Manager
You are here] B --> C[Project: Todo App
Lesson 33] B --> D[Project: Networking Library
Lesson 35] style B fill:#f90,color:#fff
Creating a Package
Create a Swift package from the command line or Xcode.
# Command line
mkdir MyLibrary
cd MyLibrary
swift package init --name MyLibrary --type library
This creates:
MyLibrary/
Package.swift
Sources/
MyLibrary/
MyLibrary.swift
Tests/
MyLibraryTests/
MyLibraryTests.swift
The generated Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "MyLibrary",
targets: ["MyLibrary"]
),
],
dependencies: [],
targets: [
.target(
name: "MyLibrary"
),
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"]
),
]
)
Building and Testing
# Build the package
swift build
# Run tests
swift test
# Build for release
swift build -c release
# Generate Xcode project
swift package generate-xcodeproj
# Show dependency graph
swift package show-dependencies
Adding Dependencies
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "WeatherKit",
platforms: [.macOS(.v13), .iOS(.v16)],
products: [
.library(name: "WeatherKit", targets: ["WeatherKit"])
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git",
from: "5.8.0"),
.package(url: "https://github.com/onevcat/Kingfisher.git",
from: "7.10.0"),
.package(path: "../SharedUtilities"),
],
targets: [
.target(
name: "WeatherKit",
dependencies: [
"Alamofire",
.product(name: "Kingfisher", package: "Kingfisher"),
"SharedUtilities"
]
),
.testTarget(
name: "WeatherKitTests",
dependencies: ["WeatherKit"]
),
]
)
Creating a Library
A sample library with multiple source files.
// Sources/WeatherKit/WeatherService.swift
import Foundation
public struct WeatherService {
private let apiKey: String
public init(apiKey: String) {
self.apiKey = apiKey
}
public func fetchWeather(for city: String) async throws -> Weather {
return Weather(temperature: 22.0, condition: "Sunny")
}
}
// Sources/WeatherKit/Weather.swift
public struct Weather: Codable, Sendable {
public let temperature: Double
public let condition: String
public init(temperature: Double, condition: String) {
self.temperature = temperature
self.condition = condition
}
}
Types that should be accessible to package consumers must be marked public or open.
Package Products
A package can produce multiple products.
let package = Package(
name: "Utilities",
products: [
.library(name: "CoreUtils", targets: ["CoreUtils"]),
.library(name: "NetworkingUtils", targets: ["NetworkingUtils"]),
.library(
name: "AllUtils",
type: .dynamic,
targets: ["CoreUtils", "NetworkingUtils"]
),
.executable(name: "CLITool", targets: ["CLITool"]),
],
targets: [
.target(name: "CoreUtils"),
.target(name: "NetworkingUtils", dependencies: ["CoreUtils"]),
.executableTarget(name: "CLITool", dependencies: ["CoreUtils"]),
]
)
Consumers choose which products to import, avoiding pulling in unnecessary dependencies.
Versioning
SPM uses semantic versioning.
// Exact version
.package(url: "...", exact: "1.2.3")
// Up to next major (1.x.x, excluding 2.0.0)
.package(url: "...", from: "1.2.3")
// Up to next minor (1.2.x, excluding 1.3.0)
.package(url: "...", .upToNextMinor(from: "1.2.3"))
// Range
.package(url: "...", "1.0.0"..<"2.0.0")
// Branch
.package(url: "...", branch: "main")
// Commit
.package(url: "...", revision: "a1b2c3d4")
For published packages, use from: or upToNextMinor:. Branch and commit dependencies are for development only.
Publishing a Package
Steps to publish your package:
# 1. Tag a version
git tag 1.0.0
git push --tags
# 2. Verify the package
swift package diagnose-api-breaking-changes 1.0.0
# 3. Create a GitHub release
gh release create 1.0.0 --notes "Initial release"
SPM resolves package URLs via GitHub tags. Users add https://github.com/yourname/YourPackage.git to their dependencies.
Resource Bundles
Packages can include resources like images, xibs, and asset catalogs.
let package = Package(
name: "MyUIComponents",
targets: [
.target(
name: "MyUIComponents",
resources: [
.process("Assets.xcassets"),
.copy("Fonts"),
.process("Nibs"),
]
),
]
)
.Process applies platform-specific processing (compiling asset catalogs). .copy copies files as-is.
Binary Frameworks
SPM supports binary dependencies for closed-source libraries.
let package = Package(
name: "AnalyticsSDK",
products: [
.library(name: "AnalyticsSDK", targets: ["AnalyticsSDK"])
],
targets: [
.binaryTarget(
name: "AnalyticsSDK",
path: "artifacts/AnalyticsSDK.xcframework"
),
// Or from a URL:
// .binaryTarget(
// name: "AnalyticsSDK",
// url: "https://example.com/AnalyticsSDK-1.0.0.xcframework.zip",
// checksum: "..."
// ),
]
)
Use swift package compute-checksum to generate the checksum for remote binary targets.
Common Mistakes
Not marking types as public: Types not marked
publicare internal to the package and invisible to consumers.Missing platform versions: Always specify minimum platform versions for packages that use platform-specific APIs.
Using branch dependencies in production: Branch dependencies break version resolution. Use tagged versions for releases.
Not running API breaking checks before release: Use
swift package diagnose-api-breaking-changesto catch accidental breaking changes.Forgetting to commit Package.resolved: The resolved file pins dependency versions for reproducible builds.
Practice Questions
- What is the difference between a library and an executable in SPM?
- How do you specify version constraints for dependencies?
- Why must you mark public API with the public keyword in a package?
- How do you include image resources in a Swift package?
- Challenge: Create a Swift package called "StringUtilities" with two targets: "Core" (string validation methods) and "Extended" (encryption helpers). The Extended target depends on Core. Publish it to a local Git Repository with version 1.0.0.
Mini Project
Create and publish a SwiftCache package that:
- Provides a generic
Cache<Key: Hashable, Value>type - Supports in-memory and disk-based storage
- Has configurable TTL (time-to-live) and max size
- Includes resource files for default configuration
- Has 80%+ test coverage
- Is published to GitHub with proper versioning and release notes
FAQ
What's Next
Apply your SPM knowledge by building the Project: Todo App or the Project: Networking Library as a reusable SPM package.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro