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.

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

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 MathKit

This 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 build

This 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 test

SPM 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

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

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

11. Practice Exercise

Build a package called TextTools with these requirements:

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.