Wes Matlock

Mastering Dependencies: A Guide to Using Swift Package Manager

Introduction


Mastering Dependencies: A Guide to Using Swift Package Manager

Introduction

In the ever-evolving landscape of iOS development, managing dependencies efficiently is crucial for maintaining a clean and scalable codebase. Swift Package Manager (SPM) has emerged as a powerful tool that simplifies this process, offering a seamless way to manage and integrate third-party libraries into your projects. In this post, we’ll delve into the essentials of Swift Package Manager, demonstrating how to harness its capabilities to streamline your development workflow.

What is Swift Package Manager?

Swift Package Manager is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. SPM makes it easy to manage both external libraries and internal components, ensuring that your projects remain modular and easy to maintain.

Setting Up Swift Package Manager

Creating a Package: To create a new Swift package, use the swift package init command followed by the type of package you want to create (e.g., library or executable).

swift package init --type library

Adding Dependencies: Dependencies can be added to your package by editing the Package.swift file. Specify the package dependencies and their versions using the dependencies array

// swift-tools-version:5.3  
import PackageDescription  
  
let package = Package(  
    name: "MyLibrary",  
    products: [  
        .library(name: "MyLibrary", targets: ["MyLibrary"]),  
    ],  
    dependencies: [  
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"),  
    ],  
    targets: [  
        .target(name: "MyLibrary", dependencies: ["Alamofire"]),  
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),  
    ]  
)

Building the Package: Build your package using the swift build command. This will compile your code and resolve any dependencies specified in your Package.swift file.

swift build

Integrating Swift Package Manager in Xcode

  1. Adding Packages: Open your Xcode project, navigate to File > Swift Packages > Add Package Dependency, and enter the URL of the package repository. Xcode will fetch the package and integrate it into your project.
  2. Managing Dependencies: Xcode provides a user-friendly interface for managing package dependencies. You can specify version rules, update packages, and remove dependencies as needed.

Advanced Usage of Swift Package Manager

Customizing Package Configuration: SPM allows for advanced configuration options, such as setting custom build settings, defining product types, and specifying resources.

  • Custom Build Settings: You can define custom build settings for your package in the Package.swift file.
.target(  
    name: "MyLibrary",  
    dependencies: ["Alamofire"],  
    path: "Sources/MyLibrary",  
    cSettings: [  
        .headerSearchPath("include"),  
        .define("MY_LIBRARY", to: "1")  
    ],  
    swiftSettings: [  
        .define("DEBUG", .when(configuration: .debug)),  
        .define("RELEASE", .when(configuration: .release))  
    ]  
)
  • Defining Resources: SPM allows you to include resources like images, data files, or localized strings in your package.
.target(  
    name: "MyLibrary",  
    resources: [  
        .process("Resources/MyImage.png"),  
        .copy("Resources/MyDataFile.dat")  
    ]  
)

Local Packages: For internal development, you can create and manage local packages by adding them directly to your project directory and specifying their path in the dependencies array.

.package(path: "../MyLocalPackage")
  • Example Project Structure:
MyProject/  
├── MyLocalPackage/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── MyLocalPackage/  
│   │       └── MyLocalPackage.swift  
├── MyProject/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── MyProject/  
│   │       └── main.swift
  • Package.swift for MyProject:
// swift-tools-version:5.3  
import PackageDescription  
  
let package = Package(  
    name: "MyProject",  
    dependencies: [  
        .package(path: "../MyLocalPackage"),  
    ],  
    targets: [  
        .target(  
            name: "MyProject",  
            dependencies: ["MyLocalPackage"]),  
    ]  
)

Command Line Tools: Use SPM to build command-line tools by specifying executable as the product type. This is useful for creating utility scripts or standalone applications.

  • Creating an Executable Package:
swift package init --type executable
  • Package.swift for Executable:
// swift-tools-version:5.3  
import PackageDescription  
  
let package = Package(  
    name: "MyTool",  
    products: [  
        .executable(name: "MyTool", targets: ["MyTool"]),  
    ],  
    targets: [  
        .target(  
            name: "MyTool",  
            dependencies: []),  
    ]  
)
  • Example main.swift for Executable:
import Foundation  
  
print("Hello, World!")

Customizing Build Scripts: SPM supports custom build scripts for tasks like generating code or performing pre/post build steps.

  • Pre-build Script Example:
// swift-tools-version:5.3  
import PackageDescription  
  
let package = Package(  
    name: "MyLibrary",  
    targets: [  
        .target(  
            name: "MyLibrary",  
            dependencies: [],  
            plugins: [  
                .plugin(name: "PreBuildScriptPlugin")  
            ]  
        )  
    ]  
)
  • Plugin Example:
// swift-tools-version:5.3  
import PackagePlugin  
  
@main  
struct PreBuildScriptPlugin: BuildToolPlugin {  
    func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {  
        return [  
            .prebuildCommand(  
                displayName: "Generating Swift Code",  
                executable: .init(path: "/usr/bin/env"),  
                arguments: ["swift", "run", "MyCodeGenerator"],  
                environment: [:]  
            )  
        ]  
    }  
}

Testing with Swift Package Manager: SPM supports unit testing, and you can use the swift test command to run tests. Tests are defined in the Tests directory.

  • Example Test:
import XCTest  
@testable import MyLibrary  
  
final class MyLibraryTests: XCTestCase {  
    func testExample() {  
        XCTAssertEqual(MyLibrary().text, "Hello, World!")  
    }  
}

Continuous Integration with Swift Package Manager: Integrate SPM with CI/CD pipelines to automate the build, test, and deployment processes.

  • Example GitHub Actions Workflow:
name: Swift  
  
on:  
  push:  
    branches: [ main ]  
  pull_request:  
    branches: [ main ]  
  
jobs:  
  build:  
    runs-on: ubuntu-latest  
  
    steps:  
    - uses: actions/checkout@v2  
    - name: Setup Swift  
      uses: fwal/setup-swift@v1  
      with:  
        swift-version: '5.3'  
    - name: Build  
      run: swift build  
    - name: Test  
      run: swift test

Best Practices for Using Swift Package Manager

Versioning: Use semantic versioning for your packages to ensure compatibility and prevent breaking changes. Semantic versioning follows the format MAJOR.MINOR.PATCH, where:

  • MAJOR version changes indicate incompatible API changes,
  • MINOR version changes add functionality in a backward-compatible manner, and
  • PATCH version changes include backward-compatible bug fixes.
  • Example of Versioning in Package.swift:
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0")
  • Setting Version Constraints: You can set specific version constraints to ensure compatibility with your package.
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0"))

Modularity: Keep your packages modular by splitting large codebases into smaller, reusable components. This promotes better organization and makes it easier to manage dependencies.

  • Example of Splitting a Large Codebase: Suppose you have a large project with multiple functionalities like networking, database management, and UI components. Instead of having everything in one package, you can create separate packages for each functionality.
MyProject/  
├── NetworkPackage/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── NetworkPackage/  
│   │       └── NetworkManager.swift  
├── DatabasePackage/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── DatabasePackage/  
│   │       └── DatabaseManager.swift  
├── UIPackage/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── UIPackage/  
│   │       └── UIComponents.swift  
├── MyProject/  
│   ├── Package.swift  
│   ├── Sources/  
│   │   └── MyProject/  
│   │       └── main.swift
  • Example Package.swift for MyProject:
// swift-tools-version:5.3  
import PackageDescription  
  
let package = Package(  
    name: "MyProject",  
    dependencies: [  
        .package(path: "../NetworkPackage"),  
        .package(path: "../DatabasePackage"),  
        .package(path: "../UIPackage"),  
    ],  
    targets: [  
        .target(  
            name: "MyProject",  
            dependencies: ["NetworkPackage", "DatabasePackage", "UIPackage"]),  
    ]  
)

Documentation: Provide comprehensive documentation and examples for your packages to help other developers integrate and use them effectively. This can include README files, inline code comments, and example projects.

  • Creating a README File: A well-documented README file should include the following sections:
  • Introduction: Briefly describe the package and its purpose.
  • Installation: Provide step-by-step instructions on how to add the package to a project.
  • Usage: Include code examples and explain how to use the package.
  • API Reference: Document the main classes, methods, and properties.
# MyLibrary  
  
## Introduction  
MyLibrary is a Swift package that provides awesome features for your iOS projects.  
  
## Installation  
To add MyLibrary to your project, include it in your `Package.swift` file:  
  
```swift  
.package(url: "https://github.com/username/MyLibrary.git", from: "1.0.0")

Automated Testing: Ensure your package has comprehensive test coverage by writing unit tests for all major functionalities. Use the swift test command to run your tests.

  • Example Unit Test:
import XCTest  
@testable import MyLibrary  
  
final class MyLibraryTests: XCTestCase {  
    func testDoSomethingAwesome() {  
        let myObject = MyLibrary()  
        XCTAssertEqual(myObject.doSomethingAwesome(), "Awesome")  
    }  
}
  • Setting Up Continuous Integration: Integrate your tests with a CI service like GitHub Actions to automate testing and ensure your package remains reliable.
name: Swift Package Tests  
  
on:  
  push:  
    branches: [ main ]  
  pull_request:  
    branches: [ main ]  

Maintaining Compatibility: Regularly update your packages to ensure compatibility with the latest versions of Swift and other dependencies. Monitor updates to third-party libraries and adjust your package’s dependencies accordingly.

  • Checking for Dependency Updates: Use tools like swift package update to check for and apply updates to your package’s dependencies.
swift package update

Publishing Packages: Make your package available to the Swift community by publishing it to a public repository like GitHub. Ensure your package meets the necessary requirements, such as having a valid Package.swift file and a semantic version tag.

Creating a Release on GitHub:

  • Tag your release using Git:
git tag 1.0.0  
git push origin 1.0.0
  • Create a release on GitHub with a detailed description of the changes and new features.

Conclusion

Swift Package Manager is a robust and versatile tool that simplifies dependency management and promotes modularity in Swift projects. By mastering its features and best practices, you can enhance your development workflow, reduce complexity, and ensure a more maintainable codebase. Start leveraging the power of Swift Package Manager today and take your iOS projects to the next level!

If you want to learn more about native mobile development, you can check out the other articles I have written here: https://medium.com/@wesleymatlock

Happy coding! 🚀

By Wesley Matlock on June 20, 2024.

Canonical link

Exported from Medium on May 10, 2025.

Written on June 20, 2024