Polyglot
Languages Swift modules
Swift § modules

Packages and SPM

The Swift ecosystem revolves around the Swift Package Manager (SPM) — the official tool for organising, building, testing, and distributing Swift code. A Swift package is a directory containing a Package.swift manifest declaring targets (modules), products (executables and libraries), and dependencies (other packages). The conventional contemporary Swift discipline uses SPM for routine project organisation; Xcode admits substantial integration with SPM packages alongside its own project structure for application work. The combination — Package.swift as the manifest, the target/product/dependency structure, the swift package command-line, the substantial Xcode integration — is the substance of Swift’s package management.

A package

The minimum package layout:

MyPackage/
├── Package.swift
├── Sources/
│   └── MyPackage/
│       └── MyPackage.swift
└── Tests/
    └── MyPackageTests/
        └── MyPackageTests.swift

The Package.swift manifest:

// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(name: "MyPackage", targets: ["MyPackage"]),
    ],
    targets: [
        .target(name: "MyPackage"),
        .testTarget(name: "MyPackageTests", dependencies: ["MyPackage"]),
    ]
)

The first line specifies the swift-tools-version — the minimum SPM/Swift compiler version required.

Creating a package

# Library:
swift package init --type library

# Executable:
swift package init --type executable

# Empty (rare):
swift package init --type empty

The conventional output:

Sources/
    MyPackage/
        MyPackage.swift
Tests/
    MyPackageTests/
        MyPackageTests.swift
Package.swift

Package.swift structure

Package metadata

let package = Package(
    name: "MyPackage",
    defaultLocalization: "en",
    platforms: [
        .iOS(.v16),
        .macOS(.v13),
        .watchOS(.v9),
        .tvOS(.v16),
    ],
    products: [...],
    dependencies: [...],
    targets: [...]
)

The platforms array admits substantial minimum-version constraints.

Products

products: [
    .library(name: "MyLibrary", targets: ["MyLibrary"]),
    .library(name: "MyLibraryDynamic", type: .dynamic, targets: ["MyLibrary"]),
    .executable(name: "myapp", targets: ["MyApp"]),
]

Products admit:

  • .library — for code consumed by other packages.
  • .executable — for command-line tools.

Library products may be static (default) or dynamic.

Targets

targets: [
    .target(
        name: "MyLibrary",
        dependencies: ["OtherLib"],
        path: "Sources/MyLibrary",
        exclude: ["README.md"],
        resources: [
            .process("Resources"),                 // bundled with the target
        ]
    ),
    .executableTarget(
        name: "MyApp",
        dependencies: ["MyLibrary"]
    ),
    .testTarget(
        name: "MyLibraryTests",
        dependencies: ["MyLibrary"]
    ),
]

Target kinds:

  • .target — library target.
  • .executableTarget — executable target.
  • .testTarget — test target.
  • .binaryTarget — pre-built binary (XCFramework).
  • .systemLibrary — wrapper for system libraries.
  • .plugin — build/command plugin.
  • .macroTarget — Swift macro (Swift 5.9+).

Dependencies

dependencies: [
    .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"),
    .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),

    // Specific version:
    .package(url: "https://github.com/example/lib", exact: "2.1.3"),

    // Branch:
    .package(url: "https://github.com/example/lib", branch: "main"),

    // Local path:
    .package(path: "../OtherPackage"),
]

Then in target dependencies:

.target(
    name: "MyLibrary",
    dependencies: [
        .product(name: "Collections", package: "swift-collections"),
        .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
        "OtherPackage",                            // local path package
    ]
),

Version specifiers

FormMeaning
from: "1.0.0">= 1.0.0, < 2.0.0 (semver-major-bound)
exact: "1.2.3"exact
"1.0.0"..."2.0.0"range
branch: "main"branch
revision: "abc123"git commit

The conventional contemporary discipline uses from: for semver-pinned dependencies.

SPM commands

# Build:
swift build                                        # debug build
swift build -c release                             # optimised build
swift build --product myapp                       # specific product

# Run:
swift run                                          # run the executable target
swift run myapp arg1 arg2                          # specific args

# Test:
swift test                                         # all tests
swift test --filter MyTest                        # specific tests

# Resolve dependencies:
swift package resolve
swift package update                              # update to latest matching versions

# Generate Xcode project:
swift package generate-xcodeproj                   # legacy; Xcode now opens Package.swift directly

# Edit dependencies:
swift package edit OtherPackage                    # opens for local development
swift package unedit OtherPackage                  # closes editing

The swift package resolve produces a Package.resolved file recording exact versions — conventionally committed to version control.

Project layouts

Library package

MyLibrary/
├── Package.swift
├── README.md
├── Sources/
│   └── MyLibrary/
│       ├── MyLibrary.swift
│       └── Internal/
│           └── Helpers.swift
└── Tests/
    └── MyLibraryTests/
        └── MyLibraryTests.swift

Executable package

MyApp/
├── Package.swift
├── README.md
├── Sources/
│   └── MyApp/
│       ├── main.swift                            # or @main type
│       └── Service.swift
└── Tests/
    └── MyAppTests/

Multi-target package

MyProject/
├── Package.swift
├── Sources/
│   ├── MyApp/                                     # executable
│   │   └── App.swift
│   ├── MyLibrary/                                  # library
│   │   ├── PublicAPI.swift
│   │   └── Internal.swift
│   └── MyCore/                                     # shared core
│       └── Models.swift
└── Tests/
    ├── MyAppTests/
    ├── MyLibraryTests/
    └── MyCoreTests/
// Package.swift
let package = Package(
    name: "MyProject",
    products: [
        .executable(name: "myapp", targets: ["MyApp"]),
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    targets: [
        .executableTarget(name: "MyApp", dependencies: ["MyLibrary"]),
        .target(name: "MyLibrary", dependencies: ["MyCore"]),
        .target(name: "MyCore"),
        .testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
        .testTarget(name: "MyCoreTests", dependencies: ["MyCore"]),
    ]
)

Resources

Bundled non-source resources:

.target(
    name: "MyApp",
    resources: [
        .process("Resources"),                     // process plist, xcassets, storyboard
        .copy("Templates"),                        // copy as-is
    ]
)

Then in code:

let url = Bundle.module.url(forResource: "config", withExtension: "json")
let data = try Data(contentsOf: url!)

The Bundle.module admits accessing target-specific resources.

Common dependencies

The conventional contemporary Swift dependencies:

  • swift-collections — additional data structures (OrderedDictionary, OrderedSet, Heap).
  • swift-async-algorithms — async sequence operations.
  • swift-numerics — additional numeric types and operations.
  • swift-argument-parser — CLI argument parsing.
  • swift-log — logging facade.
  • swift-metrics — metrics facade.
  • swift-system — low-level system APIs.

Apple’s swift- packages are the de facto standard library extensions:

.package(url: "https://github.com/apple/swift-collections", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),

Server-side Swift

For server-side work, the conventional frameworks:

  • Vapor — full-featured web framework.
  • Hummingbird — minimal, modular.
  • Smoke Framework — AWS-focused.
// Package.swift for a Vapor app:
.package(url: "https://github.com/vapor/vapor", from: "4.85.0"),

Substantial server-side Swift admits substantial deployment to Linux servers, Lambda, and similar.

Xcode integration

Xcode opens Package.swift directly:

xed Package.swift                                  # open in Xcode

In Xcode:

  • File > Add Package Dependencies — admit adding remote packages.
  • Project Editor > Package Dependencies — manage existing.
  • Build, Test, Run — Xcode commands work as expected.

For substantial application development, an Xcode project (.xcodeproj) admits SPM packages as dependencies — alongside Xcode-specific assets, schemes, etc.

Common patterns

Library with public API

// Sources/MyLibrary/PublicAPI.swift
public struct Client {
    private let connection: Connection             // hidden
    public let configuration: Configuration

    public init(configuration: Configuration) {
        self.configuration = configuration
        self.connection = Connection(config: configuration)
    }

    public func fetch(_ request: Request) async throws -> Response {
        try await connection.send(request)
    }
}

// Sources/MyLibrary/Internal/Connection.swift
internal struct Connection { /* ... */ }

Executable with @main

// Sources/MyApp/App.swift
@main
struct App {
    static func main() async throws {
        let config = try loadConfig()
        let server = Server(config: config)
        try await server.run()
    }
}

Multi-platform package

let package = Package(
    name: "MyLib",
    platforms: [
        .iOS(.v16),
        .macOS(.v13),
        .tvOS(.v16),
        .watchOS(.v9),
        .visionOS(.v1),
    ],
    /* ... */
)

Package with tests

import XCTest
@testable import MyLibrary

final class MyLibraryTests: XCTestCase {
    func testSomething() async throws {
        let client = Client(configuration: .default)
        let result = try await client.fetch(.test)
        XCTAssertEqual(result.status, 200)
    }
}

Run with swift test or via Xcode’s test runner.

For Swift 6, the new Swift Testing framework:

import Testing
@testable import MyLibrary

@Test func testSomething() async throws {
    let client = Client(configuration: .default)
    let result = try await client.fetch(.test)
    #expect(result.status == 200)
}

@Test(arguments: [1, 2, 3])
func testParameterized(_ value: Int) {
    #expect(value > 0)
}

The Swift Testing framework admits substantial improvements over XCTest.

Conditional dependencies

let package = Package(
    name: "MyLib",
    dependencies: [
        .package(url: "https://github.com/apple/swift-log", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "MyLib",
            dependencies: [
                .product(name: "Logging", package: "swift-log",
                         condition: .when(platforms: [.linux, .macOS]))
            ]
        ),
    ]
)

Resources with localisation

.target(
    name: "MyApp",
    resources: [
        .process("Localizable.strings"),
        .process("Resources"),
    ]
)

Macro target (Swift 5.9+)

import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyMacros",
    targets: [
        .macro(
            name: "MyMacrosImpl",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
            ]
        ),
        .target(name: "MyMacros", dependencies: ["MyMacrosImpl"]),
    ]
)

Plugin

.plugin(
    name: "GenerateCode",
    capability: .buildTool(),
    dependencies: ["CodeGenerator"]
)

Binary distribution

.binaryTarget(
    name: "MyXCFramework",
    url: "https://example.com/MyXCFramework-1.0.0.xcframework.zip",
    checksum: "abc123..."
)

.gitignore for Swift packages

.build/
.swiftpm/
Package.resolved
xcuserdata/
*.xcodeproj
DerivedData/

The conventional discipline:

  • Commit Package.resolved — for executables (admits reproducible builds).
  • Don’t commit Package.resolved — for libraries (admits flexibility for consumers).

A note on the conventional discipline

The contemporary Swift packages-and-SPM advice:

  • Use SPM — the standard for new projects.
  • Use semver-style versioning (from: "1.0.0") for dependencies.
  • Use Package.resolved — commit for executables, omit for libraries.
  • Use swift package init to create new packages.
  • Use @main for executable entry points.
  • Use Bundle.module for resource access.
  • Use swift-collections, swift-async-algorithms — extend the standard library.
  • Test with XCTest (current standard) or Swift Testing (5.9+).
  • Open Package.swift in Xcode for IDE integration.

The combination — Package.swift as the manifest, the target/product/dependency structure, the swift command-line, the Xcode integration, the substantial dependency ecosystem — is the substance of Swift’s package management. The discipline produces clear, well-organised, dependency-managed projects with substantial cross-platform support.