DEV Community

Wesley de Groot
Wesley de Groot

Posted on • Originally published at wesleydegroot.nl on

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

  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 :

  2. Swift Generates necessary files :

  3. A look into Package.swift :

  4. Building and Testing :

    Use the following commands to build and test your package:

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)
    ]
Enter fullscreen mode Exit fullscreen mode

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"]
        )
    ]
Enter fullscreen mode Exit fullscreen mode

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"
        )
    ]
Enter fullscreen mode Exit fullscreen mode

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"
        )
    ]
Enter fullscreen mode Exit fullscreen mode

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")
        )
    ]
Enter fullscreen mode Exit fullscreen mode

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"]
        )
    ]
Enter fullscreen mode Exit fullscreen mode

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/")
    ]
)
Enter fullscreen mode Exit fullscreen mode

Only add a specific file:

.target(
    name: "MyPackage",
    resources: [
        .process("Resources/image.png")
    ]
)
Enter fullscreen mode Exit fullscreen mode

Copy all resources found in the Resources directory:

.target(
    name: "MyPackage",
    resources: [
        .copy("Resources/")
    ]
)
Enter fullscreen mode Exit fullscreen mode

Copy a specific file:

.target(
    name: "MyPackage",
    resources: [
        .copy("Resources/image.png")
    ]
)
Enter fullscreen mode Exit fullscreen mode

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/")
    ]
)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Source: Eneko Alonso.

You can then use the extension as follows:

var body: some View {
    Image(packageResource: "sample_image", ofType: "png")
}
Enter fullscreen mode Exit fullscreen mode

For any other resources, you can rely on accessing resources directly using the Bundle:

Bundle.module.url(forResource: "sample_text_resource", withExtension: "txt")
Enter fullscreen mode Exit fullscreen mode

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

  2. Package@swift-5.7.swift

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

Top comments (0)