Wesley de Groot's Blog
Understanding Package.swift

Back

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

  1. Package Description: The top-level structure that includes metadata about the package, such as its name, platforms, and Swift tools version.
  2. Platforms (Optional): Defines the platforms which your package supports.
  3. Products: Defines the executables and libraries produced by the package. These can be used by other packages or applications.
  4. Dependencies: Lists external packages that your package depends on. SPM will fetch and manage these dependencies for you.
  5. Targets: The basic building blocks of a package. Each target can define a module or a test suite.
  6. Resources (Optional): Bundles resources with the package, such as images or sound files.
  7. 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)

  1. Creating a New Package:

    mkdir MyPackage
    cd MyPackage
    swift package init

    This command creates a new package with a default Package.swift file.

  2. 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
  3. 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"]),
        ]
    )
  4. 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

  1. Backward Compatibility: Maintain support for older Swift versions while adopting new features in newer versions.
  2. Granular Control: Tailor your package configuration to specific Swift versions, ensuring optimal performance and compatibility.
  3. 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:

  1. 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"]),
        ]
    )
  2. 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

Share


Share Mastodon Twitter LinkedIn Facebook
x-twitter mastodon github linkedin discord threads instagram whatsapp bluesky square-rss sitemap