This episode covers Swift Package Manager (SPM): its foundations and how it works, creating executable and library packages with dependencies, multi-target structure with test targets, and integrating SPM into Xcode projects for healthy modularization.

As applications grow, a single giant target becomes hard to maintain, hard to test, and hard to share. Episode 10 covers package management and modularization — the tools that solve this problem: Swift Package Manager (SPM), the official Swift dependency manager.
SPM is more than just a tool to download libraries. It is also a modern model for distributing and structuring code: applications, libraries, and tools are organized into packages that can be tested, versioned, and reused across projects and platforms. Master SPM and you open the door to the Swift open source ecosystem.
SPM is a tool from the Swift toolchain that manages three things: building source, resolving dependencies, and testing packages. Unlike CocoaPods and Carthage, SPM is integrated with the compiler, so there's no need to call a separate third-party tool.
A manifest named Package.swift is the brain of a package. All configuration is declared there:
mkdir CatatanCLI && cd CatatanCLI
swift package init --type executableswift package init --type executable generates a standard layout along with a Package.swift file and one source file. Other --type options include library, executable, and empty.
An executable package can be built and run right away:
swift build
swift run CatatanCLI
swift testswift build compiles the entire package and its dependencies, swift run CatatanCLI runs the executable, and swift test builds and runs all tests. All three use the same manifest — there is no duplicate configuration.
A modern manifest follows this format:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "CatatanCLI",
platforms: [.macOS(.v13)],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git",
from: "1.3.0")
],
targets: [
.executableTarget(
name: "CatatanCLI",
dependencies: [.product(name: "ArgumentParser",
package: "swift-argument-parser")]
)
]
).package(url:from:) adds a dependency from a git URL with a minimum version bound, and .product(name:package:) connects a library product to a target. Note the first line, // swift-tools-version, which determines the manifest version.
The following commands download and lock dependency versions:
swift package resolve
swift package show-dependenciesswift package resolve downloads dependencies and records the selected versions in Package.resolved. Always version this file so the whole team builds with identical dependencies — in CI, this makes builds reproducible.
A good package separates logic (library) from interface (executable). The library can be tested and reused; the executable only calls the library:
let package = Package(
name: "IntiCatatan",
targets: [
.target(name: "IntiCatatan"),
.executableTarget(name: "CatatanCLI",
dependencies: ["IntiCatatan"]),
.testTarget(name: "IntiCatatanTests",
dependencies: ["IntiCatatan"])
]
).target(name: "IntiCatatan") defines a library, .executableTarget the application that uses it, and .testTarget the place for tests. This separation lets you test logic without running the executable, and makes it easier to rebuild only the parts that changed.
The generated layout:
IntiCatatan/
├── Package.swift
├── Sources/
│ ├── IntiCatatan/
│ │ └── IntiCatatan.swift
│ └── CatatanCLI/
│ └── main.swift
└── Tests/
└── IntiCatatanTests/
└── IntiCatatanTests.swiftEach folder in Sources/ is one target, and each folder in Tests/ is one test target. This convention lets SPM find files automatically without extra configuration.
Xcode supports SPM natively. You add a dependency through File > Add Package Dependencies, enter a git URL or library name, then choose a version. Xcode then builds the package as part of the project with no additional setup.
Integration also works in both directions: an Xcode project can be used as an SPM dependency, so teams can share UI modules as well as logic. Episode 16 will use this pattern for modular architecture in large applications.
A few frequently used commands:
swift package dump-package
swift package describe
swift package updateswift package dump-package prints the manifest as expanded JSON, swift package describe details targets and dependencies, and swift package update updates dependencies to the newest allowed versions. Combine these with Git so manifest changes stay documented.
Tip
Test your package outside Xcode first with swift build and swift test. A package that is clean from the terminal almost always integrates smoothly into Xcode, and it means your codebase can be built in CI without a GUI.
Key takeaways:
Package.swift declares platforms, dependencies, and targets.swift build, swift run, and swift test cover the whole workflow.Package.resolved for reproducible builds.In the next episode, episode 11, we'll cover secure coding and data protection — secure coding practices and data validation, using the Keychain to store credentials safely, basic encryption and hashing in Swift, plus secure storage, sandboxing, and privacy on Apple platforms. Security starts with the code!