Understanding Package.swift
If you're diving into Swift development, you've likely encountered Package.swift
.
This file is the cornerstone of Swift Package Manager (SPM), Apple's tool for managing Swift code dependencies.
Let's explore what makes Package.swift
so essential and how you can leverage it in your projects.
What is Package.swift?
Package.swift
is a manifest file that defines the structure and dependencies of a Swift package.
It uses Swift syntax to describe the package's configuration, making it both powerful and easy to read.
This file is crucial for managing dependencies, building libraries, and sharing code across different projects.
Key Components of Package.swift/Table of Contents
- Package Description: The top-level structure that includes metadata about the package, such as its name, platforms, and Swift tools version.
- Platforms (Optional): Defines the platforms which your package supports.
- Products: Defines the executables and libraries produced by the package. These can be used by other packages or applications.
- Dependencies: Lists external packages that your package depends on. SPM will fetch and manage these dependencies for you.
- Targets: The basic building blocks of a package. Each target can define a module or a test suite.
- Resources (Optional): Bundles resources with the package, such as images or sound files.
- Advanced Topics: [Not required] Version-specific
Package.swift
files, publishing your Swift package, and more.
Why Use Swift Package Manager?
- Simplified Dependency Management: SPM handles the downloading and linking of dependencies, ensuring compatibility and reducing conflicts.
- Integration with Xcode: Xcode has built-in support for SPM, making it easy to add and manage packages directly within your project.
- Cross-Platform Support: SPM supports macOS, iOS, watchOS, and tvOS, allowing you to share code across different platforms.
Creating your first Package (Using the terminal)
-
Creating a New Package:
mkdir MyPackage cd MyPackage swift package init
This command creates a new package with a default
Package.swift
file. -
Swift Generates necessary files:
Creating library package: MyPackage Creating Package.swift Creating .gitignore Creating Sources/ Creating Sources/MyPackage/MyPackage.swift Creating Tests/ Creating Tests/MyPackage/ Creating Tests/MyPackageTests/MyPackageTests.swift
-
A look into Package.swift:
// swift-tools-version:5.6 import PackageDescription let package = Package( name: "MyPackage", products: [ .library( name: "MyPackage", targets: ["MyPackage"]), ], dependencies: [ // Dependencies for package (if any) ], targets: [ .target( name: "MyPackage", dependencies: []), .testTarget( name: "MyPackageTests", dependencies: ["MyPackage"]), ] )
-
Building and Testing:
Use the following commands to build and test your package:swift build swift test
Platforms
You can also define on which platforms your package works, this makes it easier for the users of your package to see if your package is compatible with the platform they are developing on.
The supported platforms types (2024) are:
platforms: [
.macOS(.v10_15),
.iOS(.v13)
]
Products
Products are the executables and libraries produced by the package.
You can define the products in the Package.swift
file as follows:
products: [
.library(
name: "MyPackage",
targets: ["MyPackage"]
)
]
Dependencies
Sometimes you want to use a package from someone else to help you with your project (e.g. SimpleNetworking) to make our network calls easier.
You can use a specific version of a package by specifying the version in the Package.swift
file:
dependencies: [
.package(
url: "https://github.com/0xWDG/SimpleNetworking.git",
from: "1.0.0"
)
]
You can use a specific branch of a package by specifying the branch in the Package.swift
file:
dependencies: [
.package(
url: "https://github.com/0xWDG/SimpleNetworking.git",
branch: "main"
)
]
You can use a specific commit of a package by specifying the commit in the Package.swift
file:
dependencies: [
.package(
url: "https://github.com/0xWDG/SimpleNetworking.git",
.revision("68726dd")
)
]
Targets
Targets are the basic building blocks of a package, defining a module or a test suite.
Targets can depend on other targets in this package and products from dependencies.
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MyPackage"
),
.testTarget(
name: "MyPackageTests",
dependencies: ["MyPackage"]
)
]
Resources
Bundling resources with a Swift Package
Swift Packages can contain resources that are bundled with the package.
Resources can include images, sounds, or any other files that your package needs to function correctly.
Adding resources to a Swift Package
You can add resources by updating your target definition:
Process all resources found in the Resources directory:
.target(
name: "MyPackage",
resources: [
.process("Resources/")
]
)
Only add a specific file:
.target(
name: "MyPackage",
resources: [
.process("Resources/image.png")
]
)
Copy all resources found in the Resources directory:
.target(
name: "MyPackage",
resources: [
.copy("Resources/")
]
)
Copy a specific file:
.target(
name: "MyPackage",
resources: [
.copy("Resources/image.png")
]
)
As demonstrated in the code example, there are several ways of adding resources. For most use cases, using the process rule will be sufficient. It’s essential to realize Xcode might optimize your files.
For example, it might optimize images for a specific platform. If using the original files is necessary, consider using the copy rule.
Excluding specific resources
If needed, you can exclude specific resources using the exclude definition:
.target(
name: "MyPackage",
exclude: ["Readme.md"],
resources: [
.process("Resources/")
]
)
Accessing resources in code using the module bundle
You can access any resources using the Bundle.module accessor.
Note that the module property will only become available if there are any resources rules defined in the package target.
It’s important to note that the following code won’t work for SwiftUI in packages:
var body: some View {
Image("sample_image", bundle: .module)
}
Instead, you’ll have to rely on UIKit/Appkit and load the image as follows:
import SwiftUI
struct ContentView: View {
var image: UIImage {
return UIImage(named: "sample_image", in: .module, compatibleWith: nil)!
}
var body: some View {
Image(uiImage: image)
}
}
This is unfortunate, you can also use this extension to load images in SwiftUI:
import SwiftUI
extension Image {
init(packageResource name: String, ofType type: String) {
#if canImport(UIKit)
guard let path = Bundle.module.path(forResource: name, ofType: type),
let image = UIImage(contentsOfFile: path) else {
self.init(name)
return
}
self.init(uiImage: image)
#elseif canImport(AppKit)
guard let path = Bundle.module.path(forResource: name, ofType: type),
let image = NSImage(contentsOfFile: path) else {
self.init(name)
return
}
self.init(nsImage: image)
#else
self.init(name)
#endif
}
}
Source: Eneko Alonso.
You can then use the extension as follows:
var body: some View {
Image(packageResource: "sample_image", ofType: "png")
}
For any other resources, you can rely on accessing resources directly using the Bundle:
Bundle.module.url(forResource: "sample_text_resource", withExtension: "txt")
Advanced Topics
Version-specific Package.swift Files
Benefits of Version-specific Package.swift Files
- Backward Compatibility: Maintain support for older Swift versions while adopting new features in newer versions.
- Granular Control: Tailor your package configuration to specific Swift versions, ensuring optimal performance and compatibility.
- Future-proofing: Prepare your packages for upcoming Swift releases without disrupting existing users.
Create a version-specific Package.swift
To create a version-specific Package.swift
file, you simply rename the file to include the Swift version it targets.
The format is Package@swift-<MAJOR>.<MINOR>.<PATCH>.swift
. Here are some examples:
Package@swift-5.7.swift
: Applies to all patch versions of Swift 5.7.Package@swift-5.7.1.swift
: Applies exclusively to Swift 5.7.1.Package@swift-5.swift
: Applies to all minor and patch versions of Swift 5.
Example Structure
Let's say you want to support Swift 5.6 and Swift 5.7 with different configurations. You would create two files:
-
Package@swift-5.6.swift
// swift-tools-version:5.6 import PackageDescription let package = Package( name: "MyPackage", platforms: [ .macOS(.v10_15), .iOS(.v13) ], products: [ .library( name: "MyPackage", targets: ["MyPackage"]), ], dependencies: [ // Dependencies for Swift 5.6 ], targets: [ .target( name: "MyPackage", dependencies: []), .testTarget( name: "MyPackageTests", dependencies: ["MyPackage"]), ] )
-
Package@swift-5.7.swift
// swift-tools-version:5.7 import PackageDescription let package = Package( name: "MyPackage", platforms: [ .macOS(.v11), .iOS(.v14) ], products: [ .library( name: "MyPackage", targets: ["MyPackage"]), ], dependencies: [ // Dependencies for Swift 5.7 ], targets: [ .target( name: "MyPackage", dependencies: []), .testTarget( name: "MyPackageTests", dependencies: ["MyPackage"]), ] )
Publishing your Swift Package
To publish your Swift package you can simply create a new tag on your Git repository.
As you’ve seen in the dependencies section, you can add references to dependencies using Git URLs.
To enable developers to explore packages more easily, Dave Verwer and Sven A. Schmidt have founded the Swift Package Index. You can start adding your package(s).
Wrap-up
Swift Packages are super cool and can help you manage your dependencies in a more structured way.
Setting up a Swift Package for the first time can be a bit overwhelming, but once you get the hang of it, it's a breeze.
i hope this article helped you understand the basics of Package.swift
and how you can leverage it in your projects.
if you have any questions or feedback, feel free to reach out to me on Mastodon, Twitter, or comment down below.
References
Read more
- Understanding reducers in Swift • 4 minutes reading time.
- iCloud Drive Tips & Tricks • 8 minutes reading time.
- Swipe actions in Swift • 5 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook