Skip to main content

Swift Package Versioning

Introduction

When building iOS, macOS, or any Swift-based applications, managing dependencies efficiently becomes crucial as your project grows. Swift Package Manager (SPM) provides a robust system for integrating external code libraries, but understanding how to properly version these dependencies is vital to maintaining a stable and reliable application.

In this guide, you'll learn how Swift Package Manager handles versioning, how to specify version requirements for dependencies, and best practices for maintaining your own Swift packages with proper version control.

Understanding Semantic Versioning

Swift Package Manager follows a convention called Semantic Versioning (or SemVer), which is a versioning scheme that uses a three-part number: MAJOR.MINOR.PATCH.

  • MAJOR: Incremented when you make incompatible API changes
  • MINOR: Incremented when you add functionality in a backward-compatible manner
  • PATCH: Incremented when you make backward-compatible bug fixes

For example, in version 2.6.1:

  • 2 is the major version
  • 6 is the minor version
  • 1 is the patch version

Pre-release and Build Metadata

SemVer also supports:

  • Pre-release versions: 1.0.0-alpha, 1.0.0-beta.2
  • Build metadata: 1.0.0+20130313144700

Version Requirements in Swift Packages

When adding dependencies to your Swift package or app project, you need to specify version requirements. Swift Package Manager supports several ways to define version constraints:

1. Exact Version

Use this when you need a specific version of a package:

swift
.package(url: "https://github.com/apple/swift-log.git", exact: "1.4.0")

2. Version Range

Specify a range of acceptable versions:

swift
.package(url: "https://github.com/apple/swift-log.git", "1.0.0"..<"2.0.0")

This means "any version from 1.0.0 up to, but not including, 2.0.0".

3. Up to Next Major

Most commonly used approach, specifying a minimum version and accepting any version up to the next major release:

swift
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0")

This is equivalent to "1.0.0"..<"2.0.0".

4. Up to Next Minor

Accept all patch updates but not minor or major:

swift
.package(url: "https://github.com/apple/swift-log.git", .upToNextMinor(from: "1.2.0"))

This is equivalent to "1.2.0"..<"1.3.0".

5. Branch, Revision, or Local

For development purposes, you can also specify:

swift
// Using a specific branch
.package(url: "https://github.com/apple/swift-log.git", branch: "development")

// Using a specific commit
.package(url: "https://github.com/apple/swift-log.git", revision: "a13491010b8441a46f8c15ca7925f83e52dbb8e0")

// Using a local path (for local development)
.package(path: "../MyLocalPackage")

Practical Example: Creating a Project with Dependencies

Let's create a simple project that demonstrates how to specify and manage package versions:

1. Creating a Swift Package with Dependencies

swift
// Package.swift
import PackageDescription

let package = Package(
name: "MyWeatherApp",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "MyWeatherApp",
targets: ["MyWeatherApp"]),
],
dependencies: [
// Network requests library with exact version
.package(url: "https://github.com/Alamofire/Alamofire.git", exact: "5.6.1"),

// JSON parsing library with version range
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),

// Logging library allowing minor updates
.package(url: "https://github.com/apple/swift-log.git", .upToNextMinor(from: "1.4.0")),
],
targets: [
.target(
name: "MyWeatherApp",
dependencies: [
"Alamofire",
"SwiftyJSON",
.product(name: "Logging", package: "swift-log")
]),
.testTarget(
name: "MyWeatherAppTests",
dependencies: ["MyWeatherApp"]),
]
)

2. Using the Dependencies in Your Code

swift
// Sources/MyWeatherApp/WeatherService.swift
import Foundation
import Alamofire
import SwiftyJSON
import Logging

public class WeatherService {
private let logger = Logger(label: "com.myapp.weatherservice")

public init() {}

public func fetchWeather(for city: String, completion: @escaping (Result<WeatherInfo, Error>) -> Void) {
let url = "https://api.weatherapi.com/v1/current.json"
let parameters: [String: String] = [
"key": "YOUR_API_KEY",
"q": city
]

logger.info("Fetching weather for \(city)")

AF.request(url, parameters: parameters)
.validate()
.responseData { response in
switch response.result {
case .success(let data):
do {
let json = try JSON(data: data)
let temperature = json["current"]["temp_c"].doubleValue
let condition = json["current"]["condition"]["text"].stringValue

let weather = WeatherInfo(
city: city,
temperature: temperature,
condition: condition
)

self.logger.info("Successfully fetched weather: \(temperature)°C, \(condition)")
completion(.success(weather))
} catch {
self.logger.error("JSON parsing error: \(error.localizedDescription)")
completion(.failure(error))
}

case .failure(let error):
self.logger.error("Network request failed: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
}

public struct WeatherInfo {
public let city: String
public let temperature: Double
public let condition: String
}

Package Resolution Process

When Swift Package Manager resolves your dependencies:

  1. It reads the Package.swift file to determine dependencies
  2. For each dependency, it calculates the set of versions that satisfy your requirements
  3. It tries to find a combination of package versions that satisfy all constraints
  4. The resolved versions are stored in a Package.resolved file

Package.resolved File

The Package.resolved file tracks the exact versions of dependencies that were resolved. Here's an example:

json
{
"object": {
"pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "78424be314842833c04bc3bef5b72e85fff99204",
"version": "5.6.1"
}
},
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version": "1.4.2"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state": {
"branch": null,
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version": "5.0.1"
}
}
]
},
"version": 1
}

Updating Dependencies

To update your package dependencies to the latest versions according to your specified constraints:

bash
swift package update

To update a specific dependency:

bash
swift package update Alamofire

Versioning Your Own Packages

If you're creating a Swift package that others might use, follow these best practices:

  1. Use Git tags for versioning: Create a tag for each release, following semantic versioning:
bash
git tag 1.0.0
git push --tags
  1. Document breaking changes: When releasing a new major version, provide migration guides.

  2. Keep a changelog: Document changes between versions in a CHANGELOG.md file.

Example Package Versioning Strategy

Here's an example versioning workflow for your own package:

  1. Start with version 0.1.0 during initial development
  2. Release 1.0.0 when the API is stable
  3. For bug fixes, bump patch: 1.0.1, 1.0.2, etc.
  4. For new features, bump minor: 1.1.0, 1.2.0, etc.
  5. For breaking changes, bump major: 2.0.0, 3.0.0, etc.

Common Versioning Issues and Solutions

Version Conflicts

When two packages depend on different versions of the same package:

PackageA → PackageC (1.0.0 ... 2.0.0)
PackageB → PackageC (1.5.0 ... 3.0.0)

The resolver will choose version 1.5.0 through 2.0.0 of PackageC, which satisfies both constraints.

If the constraints can't be satisfied, you'll get an error:

error: the package dependency graph contains cycles

Solution: Update one of the packages to accept a wider range of versions, or contact the package maintainers.

Using Older Swift Package Manager Features

If you need to support older Swift tools versions, specify it in your Package.swift:

swift
// swift-tools-version:5.3
import PackageDescription

let package = Package(
// Package definition here
)

Summary

Swift Package versioning is a crucial aspect of dependency management that helps maintain project stability. By understanding semantic versioning and how to specify version requirements, you can:

  • Ensure your app builds consistently across different environments
  • Benefit from bug fixes and new features in dependencies without breaking changes
  • Avoid dependency conflicts and versioning issues
  • Properly version your own packages for others to use

Remember these key points:

  • Use semantic versioning (MAJOR.MINOR.PATCH)
  • Choose the right version requirements for your dependencies
  • Keep the Package.resolved file under version control
  • Use proper versioning when releasing your own packages

Additional Resources

Exercises

  1. Create a simple Swift package with at least two dependencies using different versioning constraints.
  2. Update one of your dependencies and examine the changes in the Package.resolved file.
  3. Create your own Swift package and practice releasing different versions using Git tags.
  4. Experiment with resolving a version conflict between two dependencies that require different versions of a third package.
  5. Add a pre-release dependency to a package and observe how the Swift Package Manager handles it.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)