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.
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! 🙏