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
| Form | Meaning |
|---|---|
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 initto create new packages. - Use
@mainfor executable entry points. - Use
Bundle.modulefor resource access. - Use
swift-collections,swift-async-algorithms— extend the standard library. - Test with XCTest (current standard) or Swift Testing (5.9+).
- Open
Package.swiftin 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.