Swift package manager(SPM)

48 阅读4分钟

Intruction

SPM is similar Gradle or maven in the java ecosystem,designed for efficiently organizing and sharing codebases.You can use SPM to integrate third library or to modularize your own project.

Popular swift packages:

  1. SwiftyJSON :github.com/SwiftyJSON/…

  2. Alamofire : github.com/Alamofire/A…

The anatomy of a swift package

To understand its structured,we first need how to create a standard swift package project.

How to create a swift package

creating via CLI

Once your development environment is configured, the swift CLI is available to help you scaffold,build,and run test for your own projects.

The command to create an executable template project is as fllows:

swift package init --type executable

Build and run:

swift run

image.png

The command to create an library template is as fllows(defaule):

#The Follow command functionality equivalty to swift package init
swift package init --type library

Build and test:

# test command triggers a build automatically, so you can skip 'swift build'
swift build && swift test

image.png

Creating via Xcode

Navigation File->New->Project within Xcode to create a new Library.

image.png

image.png

If the Library is intented for specific project ,select your target project in Dropdown;otherwise,you can leave it. Note: Avoid having the library and target project open in separate Xcode windows at the same time. This can lead to selection conflicts or display glitches.

image.png

The Manifest File

The intial project structure is shown below:

image.png

  1. Package.swift Manifest file : The central configuration file that define the products of the package. A product refers to the output of the package,such as Library binary or an executable.Each product configuration(include its name and dependents)is declared here,and single package can host multiple products. Source directory
  2. Sources/spm_1Sources/calc Source Directories : source/spm_1 and Source/calc are source code directories specified in the manifest . if a path is not explicitly assigned , the system defaults to the Sources/[TargetName]convention
  3. Tests/spm_1Tests,Tests/calcTests Test Directories:Tests/spm_1TestsandTests/calcTests are test suit directories specified in the manifest. if a path is not explicitly assigned, the system defaults to the Tests/[TargetName]convetion.

Let's take a look at the Package.swift manifest: The following manifest defines two products:an executable named spm_1 and a library name calc

// swift-tools-version: 6.2

// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(

    name: "spm_1",

    products: [
        .executable(name: "spm_1", targets: ["spm_1"]),
        .library(name: "calc", targets: ["calc"])
    ],

    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.0"))
    ],

    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.
        .executableTarget(
            name: "spm_1",
            dependencies: [.product(name: "Alamofire", package: "Alamofire")],
            path: "Sources/spm_1"
        ),

        .testTarget(
            name: "spm_1Tests",
            dependencies: ["spm_1"],
            path: "Tests/spm_1Tests"
        ),

        .target(
            name: "calc",
            dependencies: [],
            path: "Sources/calc"
        ),

        .testTarget(
            name: "calcTests",
            dependencies: ["calc"],
            path: "Tests/calcTests"
        ),

    ]

)

// swift-tools-version: 6.2 It specifies the minimum required swift version. If the installed Swift toolchain is older than 6.2,the compilation will fail.

Checking our local swift version: To verify the installed Swift version on your machine,run the following command in the terminalswift -version image.png

Let's update the swift-tools-version in Package.swift to 6.5 and run the build command: swift build

image.png

//...
 Package(   
    name: "spm_1"
 )
//...

This define the name of the package. Note that the package name is allowed to be identiacal to any of the product names defined within it.

 products: [
        .executable(name: "spm_1", targets: ["spm_1"]),
        .library(name: "calc", targets: ["calc"])
    ],

This defines all products within the package. In this project, two products are specified: an executable and a libray. For the library product,name: "calc" represents the product's name, while targets:["calc"] links it to a specefic target definition. target are the build blocks that manage source files and their respective dependencies.

dependencies: [.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.0"))     ]

This section declare the external dependencies required by the package. Note that this only makes the dependency available to the package; It is not yet in use. To actually leverage a dependency,it must be explicitly referenced within the specific targes that require it.

...
  targets: [
        .executableTarget(
            name: "spm_1",
            dependencies: [.product(name: "Alamofire", package: "Alamofire")],
            path: "Sources/spm_1"
        ),
        .testTarget(
            name: "spm_1Tests",
            dependencies: ["spm_1"],
            path: "Tests/spm_1Tests"
        ),
        ...

    ]

executableTarget define a target of binary type, with its source code locald at path:Sources/spm_1. this target requires an external dependency specified as .product(name: "Alamofire", package: "Alamofire"). Here ,name: "Alamofire" refers to a specific product whith the package we previously declared in the dependencies section. The package parameter corrsponds to the dependency's identity, similar to how we defined our own Package(name: "spm_1", ...) earlier. Let's examine the Alamofire Manifest to get a clearer picture of these definitions.

image.png

If other projects wish to integrate our calc libray, the can refer to the following configuration:

// swift-tools-version: 6.2

import PackageDescription
let package = Package(
    name: "MyNewLibrary",
    products: [
        .library(
            name: "MyNewLibrary",
            targets: ["MyNewLibrary"]
        ),
    ],
    dependencies: [.package(path: "/Users/jack/Desktop/iosLear/spm_1")],
    targets: [
        .target(
            name: "MyNewLibrary",
            dependencies: [.product(name: "calc", package: "spm_1")]
        ),
    ]
)

Platform Compatibility

Suppose your project utilize API that are only available on specific OS versions.In such case,you can declare the support platforms and their minimum version in the manifest file. For instance,the Logger API is only available on macOS11 or later. If you don't explicity declare a minium version in your project,attempting to compile will result in the following error:

image.png image.png

To clarify the minimum supported version for your library, you should declare the supported platforms and their respective versions in the manifest file. this is considered a best practice in swift development.

let package = Package( 
name: "MyNewLibrary",
platforms: [ 
        .macOS(.v13) // 限制最低支持 macOS 13 (Ventura)
        .iOS(.v13)
]),

Package.resolved

Sometimes,a project declare a dependency with a flexible version range-for instance,accepting any version within amajor release,such as 5.10.1 up to 5.99.0. However,this flexibility can lead to inconsistencies across different development enviroments.> To address this,when Swift first performs dependency resolution(which can be manually triggered via swift package resolve),it generates a 'Package.resolved' file. This file record the exact version of each dependency,ensuring that all developers are working with the same codebase.

For example:

let package = Package(
    //...omitted
    dependencies: [
    //upToNextMajor indicates that any version from  5.10.0 up to (but excluding) 6.0.0 is acceptable
        .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.0"))
    ]

after execution,a Package.resolved file will be generate as follows: 执行后我们会生成的如下

{
  "originHash" : "bb9545b2cfa4ba84d0fc626cc6ab4c63e425a448966bf59ae5e32e9e73297b50",
  "pins" : [
    {
      "identity" : "alamofire",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/Alamofire/Alamofire.git",
      "state" : {
        "revision" : "7be73f6c2b5cd90e40798b06ebd5da8f9f79cf88",
        "version" : "5.11.0"
      }
    }
  ],
  "version" : 3
}

Key commands for managing dependencies:

  • swift package resolve (Resolve & Download):Use this when you have just cloned project or modified dependnecies and want to download them without performing a full build.

  • swift package update (Update & Download):Use this to update your third-part libraries to the latest available version within the permitted range defined in your manifest.