Swift Package Structure
Introduction
Swift Packages are a powerful way to organize and share code in the Swift ecosystem. Understanding the structure of a Swift package is essential for any developer looking to create modular, maintainable code. In this guide, we'll explore how Swift packages are organized, what files and directories they contain, and how the various components work together.
A well-structured Swift package makes your code more accessible, reusable, and easier to maintain. Whether you're creating a library for your own projects or sharing your work with the community, knowing how to properly structure your Swift package is a valuable skill.
Basic Package Structure
When you create a new Swift package, it follows a standardized directory structure. Let's start by examining the basic structure of a Swift package:
MyPackage/
├── Package.swift
├── README.md
├── Sources/
│ └── MyPackage/
│ ├── MyPackage.swift
│ └── OtherCode.swift
└── Tests/
└── MyPackageTests/
└── MyPackageTests.swift
Let's break down each component:
Package.swift
The Package.swift
file is the manifest file for your Swift package. It defines the package's name, dependencies, targets, products, and other metadata. This is the most important file in your package structure.
Here's a basic example of a Package.swift
file:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MyPackage",
products: [
.library(
name: "MyPackage",
targets: ["MyPackage"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
],
targets: [
.target(
name: "MyPackage",
dependencies: [.product(name: "Logging", package: "swift-log")]),
.testTarget(
name: "MyPackageTests",
dependencies: ["MyPackage"]),
]
)
The first line, // swift-tools-version:5.7
, specifies the minimum version of the Swift tools required to build your package. This ensures compatibility with the Swift Package Manager.
Sources Directory
The Sources
directory contains all the source code for your package. By convention, it includes a subdirectory with the same name as your package. This is where you'll place all your Swift code files.
Tests Directory
The Tests
directory contains all the test code for your package. Similar to the Sources
directory, it typically includes a subdirectory named after your package with "Tests" appended.
README.md
The README.md
file is a Markdown document that serves as the main documentation for your package. It should include information about what your package does, how to install it, and basic usage examples.
Advanced Package Structure
As your package grows, you might need a more sophisticated structure. Here's an expanded view of a more complex Swift package:
ComplexPackage/
├── Package.swift
├── README.md
├── Sources/
│ ├── ComplexPackage/
│ │ ├── ComplexPackage.swift
│ │ ├── Models/
│ │ │ ├── User.swift
│ │ │ └── Product.swift
│ │ ├── Services/
│ │ │ ├── NetworkService.swift
│ │ │ └── DatabaseService.swift
│ │ └── Utilities/
│ │ └── Logger.swift
│ └── ComplexPackageCLI/
│ └── main.swift
├── Tests/
│ ├── ComplexPackageTests/
│ │ ├── ModelTests/
│ │ │ ├── UserTests.swift
│ │ │ └── ProductTests.swift
│ │ └── ServiceTests/
│ │ └── NetworkServiceTests.swift
│ └── ComplexPackageCLITests/
│ └── CLITests.swift
├── Documentation/
│ ├── GettingStarted.md
│ └── APIReference.md
└── Examples/
├── SimpleExample/
│ └── main.swift
└── ComplexExample/
└── main.swift
This structure includes:
- Multiple targets within the
Sources
directory - Organized subdirectories for different types of code (Models, Services, Utilities)
- Dedicated documentation and examples directories
- Structured test directories that mirror the source code organization
Creating Multiple Targets
Swift packages can contain multiple targets, which are the basic building blocks for organizing your code. Each target represents a discrete unit of functionality.
Here's how you can define multiple targets in your Package.swift
file:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MultiTargetPackage",
products: [
.library(name: "CoreFeatures", targets: ["CoreFeatures"]),
.library(name: "ExtraFeatures", targets: ["ExtraFeatures"]),
.executable(name: "PackageCLI", targets: ["PackageCLI"])
],
dependencies: [],
targets: [
.target(
name: "CoreFeatures",
dependencies: []),
.target(
name: "ExtraFeatures",
dependencies: ["CoreFeatures"]),
.executableTarget(
name: "PackageCLI",
dependencies: ["CoreFeatures", "ExtraFeatures"]),
.testTarget(
name: "CoreFeaturesTests",
dependencies: ["CoreFeatures"]),
.testTarget(
name: "ExtraFeaturesTests",
dependencies: ["ExtraFeatures"])
]
)
For this configuration, your directory structure would look like:
MultiTargetPackage/
├── Package.swift
├── Sources/
│ ├── CoreFeatures/
│ │ └── CoreFeatures.swift
│ ├── ExtraFeatures/
│ │ └── ExtraFeatures.swift
│ └── PackageCLI/
│ └── main.swift
└── Tests/
├── CoreFeaturesTests/
│ └── CoreFeaturesTests.swift
└── ExtraFeaturesTests/
└── ExtraFeaturesTests.swift
Resources in Swift Packages
Starting from Swift 5.3, you can include resources in your Swift packages. Resources can be images, data files, storyboards, or any other asset your package might need.
To include resources in your package, update your target in the Package.swift
file:
.target(
name: "MyPackage",
dependencies: [],
resources: [
.process("Resources/Images"),
.copy("Resources/Data/sample.json")
]
)
The directory structure would look like:
MyPackage/
├── Package.swift
├── Sources/
│ └── MyPackage/
│ ├── MyPackage.swift
│ └── Resources/
│ ├── Images/
│ │ └── logo.png
│ └── Data/
│ └── sample.json
└── Tests/
└── MyPackageTests/
└── MyPackageTests.swift
You can then access these resources in your code:
let logoURL = Bundle.module.url(forResource: "logo", withExtension: "png", subdirectory: "Images")
let sampleDataURL = Bundle.module.url(forResource: "sample", withExtension: "json", subdirectory: "Data")
Real-World Example: Creating a Networking Library
Let's create a practical example of a Swift package structure for a networking library:
NetworkKit/
├── Package.swift
├── README.md
├── Sources/
│ └── NetworkKit/
│ ├── NetworkKit.swift
│ ├── Core/
│ │ ├── NetworkManager.swift
│ │ ├── URLRequestBuilder.swift
│ │ └── NetworkResponse.swift
│ ├── Models/
│ │ ├── HTTPMethod.swift
│ │ └── NetworkError.swift
│ ├── Extensions/
│ │ └── URLSession+Async.swift
│ └── Mocks/
│ └── MockURLSession.swift
└── Tests/
└── NetworkKitTests/
├── NetworkManagerTests.swift
├── URLRequestBuilderTests.swift
└── Mocks/
└── MockData.swift
The Package.swift
file might look like:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "NetworkKit",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
name: "NetworkKit",
targets: ["NetworkKit"]),
],
dependencies: [],
targets: [
.target(
name: "NetworkKit",
dependencies: []),
.testTarget(
name: "NetworkKitTests",
dependencies: ["NetworkKit"]),
]
)
Now let's implement a basic version of our network library:
// NetworkKit.swift
public struct NetworkKit {
public static let version = "1.0.0"
public static func configure() {
print("NetworkKit \(version) configured")
}
}
// Core/NetworkManager.swift
import Foundation
public class NetworkManager {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func fetch<T: Decodable>(url: URL) async throws -> T {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decodingError(error)
}
}
}
// Models/NetworkError.swift
import Foundation
public enum NetworkError: Error {
case invalidURL
case invalidResponse
case requestFailed(Error)
case decodingError(Error)
case unauthorized
}
// Extensions/URLSession+Async.swift
import Foundation
@available(iOS 13.0, macOS 10.15, *)
extension URLSession {
func data(from url: URL) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
let task = self.dataTask(with: url) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let data = data, let response = response else {
continuation.resume(throwing: NetworkError.invalidResponse)
return
}
continuation.resume(returning: (data, response))
}
task.resume()
}
}
}
This example demonstrates a well-structured package with clear separation of concerns, making it easier to maintain and extend.
Best Practices for Package Structure
-
Follow the Standard Structure: Adhere to Swift Package Manager conventions. This makes your package more familiar to other developers.
-
Use Meaningful Directories: Organize your code into directories that reflect their purpose (Models, Services, etc.).
-
Keep Related Code Together: Group files that work together in the same directory.
-
Mirror Your Source Structure in Tests: Organize your test files to mirror your source structure for easier navigation.
-
Minimize Public API Surface: Only expose what's necessary for users of your package.
-
Document Everything: Include clear documentation in your package, both in the code and in external files like README.md.
-
Version Control: Use semantic versioning for your package releases.
-
Separate Concerns: Consider creating multiple targets if your package has distinct sets of functionality.
Summary
Understanding Swift package structure is fundamental to creating well-organized, maintainable Swift packages. In this guide, we've explored:
- The basic structure of a Swift package
- How to create more complex package structures
- Working with multiple targets
- Including resources in your package
- A real-world example of a networking library
By following the conventions and best practices outlined in this guide, you'll be able to create Swift packages that are easy to use, maintain, and share with others.
Additional Resources
Here are some resources to further explore Swift package structure:
- Swift Package Manager Documentation
- Apple's Swift Packages Guide
- Swift Evolution Proposal: Resources in Swift Packages
Exercises
- Create a simple Swift package with a single target and implement a basic utility function.
- Expand your package to include multiple targets with dependencies between them.
- Add resources to your package and write code to access them.
- Create a Swift package with a command-line interface using an executable target.
- Implement a package with extensive testing, including mock objects and test fixtures.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)