Swift Package Manager (SPM): Packages, Targets, and Dependencies
Swift Package Manager, often called SPM, is Apple’s built-in tool for defining, building, testing, and sharing Swift code as reusable packages. It helps you organize code into modules, manage dependencies, and use the same package format across command line tools, server-side Swift, libraries, and app projects.
Quick answer: SPM uses a Package.swift manifest to describe packages, targets, products, and dependencies. You use it to build and test Swift code, and to add reusable code to Xcode or other Swift projects.
Difficulty: Intermediate
You'll understand this better if you know: basic Swift syntax, what modules and functions are, and how Xcode or the command line builds a project.
1. What Is Swift Package Manager (SPM)?
Swift Package Manager is the standard toolchain for describing and distributing Swift code. A package usually contains one or more targets, and each target becomes a module that can be built, tested, and imported by other code.
- It uses a manifest file named Package.swift.
- It can define library products, executables, tests, and plugins.
- It resolves external dependencies from remote repositories.
- It works from the command line and inside Xcode.
- It is the main packaging format for modern Swift libraries.
SPM is not a full app framework. It does not replace Xcode for UI design, signing, or app deployment. Instead, it focuses on package structure, dependency resolution, and build orchestration.
2. Why Swift Package Manager Matters
SPM matters because it gives Swift projects a consistent way to share code without manual project setup. Instead of copying source files between apps, you can create a package and reuse it across multiple projects.
It also improves maintainability. Dependencies are described in code, versioned in source control, and resolved reproducibly. That makes builds easier to understand and easier to automate in CI.
For teams, SPM reduces friction when adding shared logic such as networking layers, formatting utilities, feature flags, or test helpers. For individual developers, it makes it simple to create a command line tool or library with a standard structure.
3. Basic Syntax or Core Idea
The heart of SPM is the manifest file. It tells Swift what the package contains, how the code is grouped, and what other packages it depends on.
Minimal package manifest
This example shows the simplest useful shape of a package manifest for a library target and a test target.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MathKit",
products: [
.library(name: "MathKit", targets: ["MathKit"])
],
targets: [
.target(name: "MathKit"),
.testTarget(name: "MathKitTests", dependencies: ["MathKit"])
]
)The swift-tools-version line sets the minimum toolchain version allowed to interpret the manifest. The Package value describes the package, while products and targets define what the package exposes and how it is built.
What the main pieces mean
- Products are what other packages or apps consume.
- Targets are buildable units of code.
- Dependencies point one target to another target or package.
- Tests usually live in a separate test target.
4. Step-by-Step Examples
Example 1: Create a new package from the command line
Use the Swift command-line tool when you want a package without opening Xcode first. This is a common starting point for libraries and tools.
swift package init --type library --name MathKitThis creates a starter folder with Package.swift, source files, and tests. From there, you can edit the manifest and add your own code.
Example 2: Build the package
After changing the code or manifest, build the package locally to catch errors early.
swift buildThis compiles the package and writes build artifacts into the local build directory. If the package has dependencies, SPM resolves them before compilation.
Example 3: Run the test suite
Testing is one of the most important parts of package development. A package with tests is easier to trust and easier to refactor.
swift testSPM discovers test targets and executes them using the Swift testing infrastructure available for your toolchain. A failing test points to a behavior problem in the package, not just a build problem.
Example 4: Add a dependency from Git
Most real packages depend on other packages. You declare the remote repository and a version rule in the manifest.
let package = Package(
name: "WeatherKit",
dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")
],
targets: [
.target(
name: "WeatherKit",
dependencies: [
.product(name: "Collections", package: "swift-collections")
]
)
]
)This example shows the two-step dependency model: first declare the package URL and version rule, then reference a product from that package inside a target.
Example 5: Create an executable package
SPM is also useful for command line tools. In an executable package, the package exposes a runnable product instead of a library.
let package = Package(
name: "Greeter",
products: [
.executable(name: "Greeter", targets: ["Greeter"])
],
targets: [
.executableTarget(name: "Greeter")
]
)This package can be built and run as a command line app, which is one of the most common uses for SPM outside libraries.
5. Practical Use Cases
- Build a reusable Swift library for formatting dates, validating input, or modeling API data.
- Create a small command line tool for automation, code generation, or maintenance scripts.
- Share a feature module across multiple iOS or macOS apps.
- Manage third-party dependencies such as collections, networking helpers, or parsing libraries.
- Write package tests that run in continuous integration without a full app project.
- Separate a large codebase into smaller modules that compile more independently.
6. Common Mistakes
Mistake 1: Defining a target but forgetting to expose a product
A target can compile successfully, but other packages cannot import it unless the package exports a product. This is a very common source of confusion when a local package is added to a project.
Problem: The code builds inside the package, but consumers cannot see the module because no library product was declared.
let package = Package(
name: "MathKit",
targets: [
.target(name: "MathKit")
]
)Fix: Add a product that points at the target you want to share.
let package = Package(
name: "MathKit",
products: [
.library(name: "MathKit", targets: ["MathKit"])
],
targets: [
.target(name: "MathKit")
]
)The product makes the target importable by external code.
Mistake 2: Using the wrong target name in a dependency list
SPM is strict about target names. If a test target or feature target points to a name that does not match exactly, resolution fails during manifest evaluation or build time.
Problem: The dependency name does not match the actual target name, so SPM cannot wire the modules together.
let package = Package(
name: "MathKit",
products: [
.library(name: "MathKit", targets: ["MathKit"])
],
targets: [
.target(name: "MathKitCore"),
.testTarget(name: "MathKitTests", dependencies: ["MathKit"])
]
)Fix: Make the target name and all references match exactly.
let package = Package(
name: "MathKit",
products: [
.library(name: "MathKit", targets: ["MathKitCore"])
],
targets: [
.target(name: "MathKitCore"),
.testTarget(name: "MathKitTests", dependencies: ["MathKitCore"])
]
)Correct naming avoids confusing dependency errors and makes the package structure easier to follow.
Mistake 3: Declaring a dependency but not adding its product to the target
Adding a package to dependencies does not automatically import its code into every target. The target must explicitly depend on one or more products from that package.
Problem: The package is known to SPM, but the target does not list the product it wants to use, so imports fail.
let package = Package(
name: "WeatherKit",
dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")
],
targets: [
.target(name: "WeatherKit")
]
)Fix: Reference the package product in the target dependency list.
let package = Package(
name: "WeatherKit",
dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")
],
targets: [
.target(
name: "WeatherKit",
dependencies: [
.product(name: "Collections", package: "swift-collections")
]
)
]
)The fixed version works because package availability and target-level usage are two separate steps in SPM.
7. Best Practices
Practice 1: Keep package boundaries small and purposeful
A package is easier to maintain when each target has a clear job. Small targets compile faster and make dependencies easier to reason about.
// Better: a focused package with clear modules
let package = Package(
name: "NetworkingKit",
products: [
.library(name: "NetworkingKit", targets: ["NetworkingKit"])
],
targets: [
.target(name: "NetworkingKit")
]
)Smaller boundaries make it easier to test, reuse, and replace parts of your code later.
Practice 2: Use version rules intentionally
Your dependency rule controls how aggressively SPM updates a package. Choose the least permissive rule that still fits your maintenance plan.
let dependency = .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")A safer alternative is to use exact versions only when you need a fully locked dependency for reproducibility or compatibility testing.
let dependency = .package(url: "https://github.com/apple/swift-collections.git", exact: "1.1.0")Version rules affect upgrade behavior, so treat them as part of your release strategy.
Practice 3: Separate library code from test-only helpers
Put reusable production code in normal targets and keep test helpers in the test target when they are not needed by app code.
let package = Package(
name: "MathKit",
products: [
.library(name: "MathKit", targets: ["MathKit"])
],
targets: [
.target(name: "MathKit"),
.testTarget(name: "MathKitTests", dependencies: ["MathKit"])
]
)This keeps test utilities from leaking into production APIs and lowers the chance of accidental coupling.
8. Limitations and Edge Cases
- SPM is excellent for Swift code, but it is not a full replacement for app project management in every scenario.
- Some Xcode app features, signing settings, and resource workflows are still easier to manage in Xcode projects than in standalone packages.
- Package resolution depends on network access when dependencies are not already cached.
- Binary compatibility across toolchains can be tricky if a package uses newer language features or compiler versions.
- Resource handling works, but it behaves differently from older manual bundle setups and can confuse beginners.
- Not all build settings from an Xcode project translate directly into a package manifest.
- Package graphs can become slow or harder to reason about when many transitive dependencies are involved.
One common surprise is that a package may build locally but fail in another environment if the toolchain is too old. Another is that a package can exist in Xcode but still not be importable until its product and target wiring are correct.
9. Practical Mini Project
Let’s build a small package named GreetingKit that exposes a reusable greeting function and includes tests. This demonstrates the basic package structure, a library product, and a test target working together.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "GreetingKit",
products: [
.library(name: "GreetingKit", targets: ["GreetingKit"])
],
targets: [
.target(name: "GreetingKit"),
.testTarget(name: "GreetingKitTests", dependencies: ["GreetingKit"])
]
)Inside Sources/GreetingKit/Greeting.swift, you could define the reusable API:
public func greeting(name: String) -> String {
return "Hello, \(name)!"
}And inside Tests/GreetingKitTests/GreetingKitTests.swift, you can verify the behavior:
import Testing
@testable import GreetingKit
@Test func testGreeting() {
let result = greeting(name: "Ava")
#expect(result == "Hello, Ava!")
}This mini project shows the full package flow: define the manifest, implement a public API, and test the module from a separate test target.
10. Key Points
- SPM is Swift’s standard system for packages, targets, and dependencies.
- Package.swift describes what the package contains and how it is built.
- Products are what other code imports; targets are the build units underneath them.
- Dependency declarations and target dependencies are separate steps.
- SPM works well for libraries, command line tools, and shared modules.
- Many common problems come from mismatched names or missing product declarations.
11. Practice Exercise
Build a package called TextTools with these requirements:
- Create one library target named TextTools.
- Expose the target as a library product.
- Add a test target that depends on the library target.
- Write a function that trims whitespace from a string and returns the result.
- Write a test that checks the function with leading and trailing spaces.
Expected output: The test should pass and the function should return the trimmed text.
Hint: Use String methods from the standard library and make sure the function is public if it must be used outside the module.
Solution:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "TextTools",
products: [
.library(name: "TextTools", targets: ["TextTools"])
],
targets: [
.target(name: "TextTools"),
.testTarget(name: "TextToolsTests", dependencies: ["TextTools"])
]
)public func trimmedText(_ text: String) -> String {
return text.trimmingCharacters(in: .whitespacesAndNewlines)
}import Testing
@testable import TextTools
@Test func testTrimmedText() {
let result = trimmedText(" Hello, SPM! ")
#expect(result == "Hello, SPM!")
}12. Final Summary
Swift Package Manager is the core packaging and dependency system for Swift. It gives you a standard way to define packages, organize targets, export products, and manage external dependencies without manually wiring everything by hand.
Once you understand the relationship between products, targets, and dependencies, SPM becomes a straightforward tool for libraries, command line apps, and shared modules. Most beginner problems come from mismatched names, missing products, or forgetting that package-level dependencies do not automatically become target-level imports.
If you want to go further, the next useful topics are package resources, binary targets, and package plugins. Those features build on the same manifest concepts covered here and are the natural next step after you are comfortable with the basics.