Skip to main content

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
// 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:

  1. Multiple targets within the Sources directory
  2. Organized subdirectories for different types of code (Models, Services, Utilities)
  3. Dedicated documentation and examples directories
  4. 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
// 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:

swift
.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:

swift
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
// 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:

swift
// NetworkKit.swift
public struct NetworkKit {
public static let version = "1.0.0"

public static func configure() {
print("NetworkKit \(version) configured")
}
}
swift
// 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)
}
}
}
swift
// Models/NetworkError.swift
import Foundation

public enum NetworkError: Error {
case invalidURL
case invalidResponse
case requestFailed(Error)
case decodingError(Error)
case unauthorized
}
swift
// 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

  1. Follow the Standard Structure: Adhere to Swift Package Manager conventions. This makes your package more familiar to other developers.

  2. Use Meaningful Directories: Organize your code into directories that reflect their purpose (Models, Services, etc.).

  3. Keep Related Code Together: Group files that work together in the same directory.

  4. Mirror Your Source Structure in Tests: Organize your test files to mirror your source structure for easier navigation.

  5. Minimize Public API Surface: Only expose what's necessary for users of your package.

  6. Document Everything: Include clear documentation in your package, both in the code and in external files like README.md.

  7. Version Control: Use semantic versioning for your package releases.

  8. 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:

Exercises

  1. Create a simple Swift package with a single target and implement a basic utility function.
  2. Expand your package to include multiple targets with dependencies between them.
  3. Add resources to your package and write code to access them.
  4. Create a Swift package with a command-line interface using an executable target.
  5. 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! :)