Post

Swift Package Target Creation with a Builder Pattern

Learn how to create Swift Package targets with a builder pattern to keep your Package.swift file clean and organized.

Swift Package Target Creation with a Builder Pattern

When defining multiple Swift Package targets—especially with various dependencies, paths, and resources—your Package.swift can get cluttered. A builder pattern can streamline this process.

Traditional Approach

Typically, you define targets directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: [],
            path: "Sources/MyLibrary",
            resources: [.process("Assets")]
        ),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary"],
            path: "Tests/MyLibraryTests"
        )
    ]

As the project grows, repeated parameters become unwieldy.

Builder Pattern

Create a builder type to encapsulate target configuration. Let’s define an enum for resources:

1
2
3
4
enum ResourceInfo {
    case none
    case process(String)
}

Then, a TargetCreator struct to compose targets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import PackageDescription

public struct TargetCreator {
    private var name = ""
    private var dependencies: [PackageDescription.Target.Dependency] = []
    private var path = ""
    private var resourceInfo: ResourceInfo = .none

    var target: PackageDescription.Target {
        switch resourceInfo {
        case .none:
            return .target(name: name, dependencies: dependencies, path: path)
        case .process(let resourcesPath):
            return .target(
                name: name,
                dependencies: dependencies,
                path: path,
                resources: [.process(resourcesPath)]
            )
        }
    }

    func addName(_ newName: String) -> Self {
        var new = self
        new.name = newName
        return new
    }

    func addDependencies(_ newDeps: [PackageDescription.Target.Dependency]) -> Self {
        var new = self
        new.dependencies.append(contentsOf: newDeps)
        return new
    }

    func addPath(_ newPath: String) -> Self {
        var new = self
        new.path = newPath
        return new
    }

    func addResources(_ info: ResourceInfo) -> Self {
        var new = self
        new.resourceInfo = info
        return new
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let libraryTarget = TargetCreator()
    .addName("MyLibrary")
    .addPath("Sources/MyLibrary")
    .addResources(.process("Assets"))
    .target

let testTarget = TargetCreator()
    .addName("MyLibraryTests")
    .addDependencies([libraryTarget])
    .addPath("Tests/MyLibraryTests")
    .target

let package = Package(
    name: "MyProject",
    products: [
        .library(name: "MyLibrary", targets: [libraryTarget])
    ],
    targets: [
        libraryTarget,
        testTarget
    ]
)

Advantages

  • Readability: Each step is explicit (addName, addPath), reducing parameter clutter.
  • Consistency: Centralizes logic, reducing repetition and mistakes.
  • Expandability: New parameters (e.g., cSettings) can be added without changing every target.

By encapsulating target creation, you keep Package.swift concise and easier to maintain. If your package grows or you add more advanced configurations, you can extend the builder instead of modifying scattered initializers throughout your file.

☕ Support My Work

If you found this post helpful and want to support more content like this, you can buy me a coffee!

Your support helps me continue creating useful articles and tips for fellow developers. Thank you! 🙏

This post is licensed under CC BY 4.0 by the author.