Swift Package Manager 使用

11,916 阅读13分钟
  • Swift Package Manager 是一个苹果官方出的管理源代码分发的工具,目的是更简单的使用别人共享的代码。它会直接处理包之间的依赖管理、版本控制、编译和链接。从总体功能上来说,和 iOS 平台上的 Cocoapods、Carthage 一样。
  • Swift Package Manager (SwiftPM) 是 Apple 推出的一个包管理工具, 用于创建, 使用 Swift 的库, 以及可执行程序的工具.
  • Xcode 11 开始自集成了 libSwiftPM,这样一来,iOS、watchOS、tvOS 等平台也都可以使用了

SwiftPM 包使用步骤如下:

1. SwiftPM 包的创建

2. 配置信息(添加依赖, target配置, 添加资源文件, 本地化多语言, 支持系统版本配置)

3. 开发编译测试

4. SPM包上传到云端

5. 在Swift项目中使用

1. SwiftPM的创建

SwiftPM 管理的每个 Package相当于 Xcode.Project,并且有具体的代码定义,包的目录下必须含有 Package.swift 和 Sources代码文件夹(链接系统的包除外)。Package.swift 是整个 Package 的配置项,类似 Cocoapods 中 .podspec 和 .podfile 的集合体。下面介绍下 SwiftPM 的简单创建和使用。

创建一个可执行的包

执行以下命令

mkdir MyPackage
➜  cd MyPackage
➜  swift package init --type executable
➜  swift build
➜  swift run
Hello, World!

--type 参数

  • empty(空包):
    • Source 文件夹下什么都没有,也不能编译
  • library(静态包):
    • Source 文件夹下有个和包同名 swift 文件,里面有个空结构体
  • executable(可执行包):
    • Source 文件夹下有个 main.swift 文件,在 build 之后会在 .build/debug/ 目录下生成一个可执行文件,可通过 swift run 或者直接点击运行,从而启动一个进程
  • system-module(系统包):
    • 这种包是专门为了链接系统库(例如 libgit、jpeglib、mysql 这种系统库)准备的,本身不需要任何代码,所以也没有 Source 文件夹,但是需要编辑 module.modulemap 文件去查找系统库路径 (Swift 4.2 已经被其他方式取代)

2. 配置信息

添加依赖

如果需要依赖其他的包, 需要在 Package.swift 定义依赖项和版本,像下面这样:

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

import PackageDescription

let package = Package(
    name: "WYMobileWebsite",
    dependencies: [
        .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0"),
        .package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", from: "3.0.0"),
        .package(url:"https://github.com/PerfectlySoft/Perfect-Session.git", from: "3.0.0")
    ],
      targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "WYMobileWebsite",
            dependencies: ["PerfectHTTPServer","PerfectMySQL","PerfectSession"]),
        .testTarget(
            name: "WYMobileWebsiteTests",
            dependencies: ["WYMobileWebsite"]),
    ]
)

Package.dependencies 用于添加包的依赖,一般是包括指向包源的 git 路径和版本环境,或指向依赖包的本地路径.
在执行 Swift build 时会自动执行一个 swift package resolve 命令,该命令会解析 Package.swift 的依赖,并生成对应的 package.resolved 文件,下面有介绍。

添加依赖支持如下五种方式

直接从枚举的定义中就可以看出 Package.dependencies 支持如下五种方式:

  • git 源 + 确定的版本号
  • git 源 + 版本区间
  • git 源 + Commit 号
  • git 源 + 分支名
  • 本地路径
.package(url: "https://github.com/Alamofire/Alamofire.git", .exact("1.2.3")),
.package(url:"https://github.com/Alamofire/Alamofire.git", .branch("master")),
.package(url:"https://github.com/Alamofire/Alamofire.git", from: "1.2.3"),
.package(url: "https://github.com/Alamofire/Alamofire.git", .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"),
.package(url: "https://github.com/Alamofire/Alamofire.git", "1.2.3"..."4.1.3"),
.package(path: "../Foo"),

添加系统依赖包(不做介绍)

Package.SupportedPlatform(系统支持版本)

这个 Struct 用于设置包的最小依赖平台版本,具体 API 定义可以进入代码文档中查看,下面给出示例:

    platforms: [.macOS(.v10_10)],

需要注意的是虽然这个属性是个数组,但是目的是为了让设置不同平台的最小依赖,如果设置了多个同平台的值进去,就会报错,例如这样:[.macOS(.v10_10), .macOS(.v10_11)]

error: manifest parse error(s):found multiple declaration for the platform: macOS

Package.Product

Product 是 Package 编译后对外的产品输出,一般可分为两种类型:

  • 可执行文件
  • 静态库或者动态库

[图片上传失败...(image-a7515d-1596512368229)]

当执行完 Swift build 之后,就会在 .build/debug 下生成对应的可执行文件 tool 和静态库 libPaperStatic.a、动态库 libPaperDynamic.dylib。

Package.Target

target 是 Package 的基本构件,和 xcodeproject 一样,Package 可以有多个 target。

target 分为三种类型:

  • 常规型、
  • 测试类型、
  • 系统库类型。

分别对应下面几个快捷创建方式:

/// The type of this target.

public enum TargetType : String, Encodable {
    case regular
    case test
    case system
}

public static func target(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target

public static func testTarget(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target

public static func systemLibrary(name: String, path: String? = nil, pkgConfig: String? = nil, providers: [PackageDescription.SystemPackageProvider]? = nil) -> PackageDescription.Target=

先介绍下 Target 的几个主要的属性

  • name: 名字
  • dependencies:
    • 依赖项,注意不要和上面的 Package.Dependency 搞混了,不是一个东西,这里可以依赖上面 Package.Dependency 的东西或者依赖另一个 target。所以这里只需要写 Package 或者 Target 的名字字符串(Target.Dependency 这个枚举也实现了 ExpressibleByStringLiteral)。
  • path:
    • target 的路径,默认的话是 [PackageRoot]/Sources/[TargetName]。
  • source:
    • 源文件路径,默认 TargetName 文件夹下都是源代码文件,会递归搜索
  • exclude:
    • 需要被排除在外的文件/文件夹,这些文件不会参与编译。
  • publicHeadersPath:
    • C 家族库的公共头文件地址。
  • swiftSettings:
    • 定义一个用于特定环境(例如 Debug)的宏,需要设置的话可以去 API 上研究下
  • linkerSettings:
    • 用于链接一些系统库

添加资源文件

从更新到现在,SwiftPM 令人诟病的一个问题就是无法在包里添加资源文件。

配置要求

  • Swift 5.3
  • Xcode 12

对于一些使用目的明确的文件类型,比如下面图中的这些。开发者不需要在 package.swift 文件中配置任何东西,因为 Xcode 知道这些类型的文件是代表什么,比如 .xcassets 文件代表图片、颜色资源, xib 代表用户界面文件等

image.png

而对于一些使用目的不太明确的文件类型(如下图中的一些文件类型),则需要在 package.swift 文件中配置。例如纯文本文件,这种文件中的数据可能是需要在运行时被加载而计算或者展示,也可能只是一个开发者文档。

image.png

对于上面这种意义不明的文件,就需要在 package.swift 清单中根据规则配置,下面以这个 GameLogin 作为例子:

image.png image.png

  • 对于 Media.xcasset 和 main.storyboard 文件,Xcode 能明确知道它代表什么,所以不需要在这个配置文件中标记
  • internal Note.txt 文件和 Artwork Creation 文件夹是模块内部文件,所以写在 target 的 exclude 属性中,这样 Xcode 就不会把它编译到包里
  • 其他不能自动识别的类型并且需要被加载到 package 里的文件则配置在 resource 属性中。

上面就是配置资源文件的一些规则,其中我们可以看到对于 resource 属性,有两个静态方法: process() 和 copy() 。根据 session 中的介绍, process() 是推荐的方式,它所配置的文件会根据具体使用的平台和内置规则进行适当的优化。比如在运行时将 storyboard 或者 asset catalog 转换成适当的形式,也包括压缩图片等。如果文件类型无法识别,或者不能根据平台做任何优化,就只会被简单的拷贝,也就是 copy() 。

构建过程

当一个 App 使用 package 时,这个 package 包括源文件和资源文件。在编译时首先会将 Package 中每个 target 的源文件编译成 module 链接到 App 中,然后这些 target 中的资源文件则会被加工成 bundle 放到这些 module 中。

image.png

在 Apple 平台中,App 和 App extension 都是 bundle 集合,这些 package 的 bundle 就是 App 的一部分,所以不需要做其他处理,就能在运行时获取这些 bundle。 当被编译到一个 unbundle 产物时,比如脚本工具,则需要在脚本启动的同时加载资源 bundle(这一步的具体步骤还不太理解)

访问资源文件

在编译有资源文件的 Package 中,会自动创建并添加到 module 中一个文件: resource_bundle_accessor.swift ,里面的内容大概等价于下面这样:

import Foundation
extension Bundle {
	static let module = Bundle(path: "\(Bundle.main.bundlePath)/path/to/this/targets/resource/bundle")
}

对于 Swift 和 OC 分别可以使用下面这种方式,当然也可以使用 UIImage 自己的带有 Bundle 参数的 Api

image.png

由于 module 是内部属性,所以这种方式只能访问自己模块内部的资源文件,无法跨模块访问。如果想在一个公共模块提供外部模块使用的资源,则需要自己创建一个资源访问器。关于这一点,使用过 Cocoapods 的 resource_bundle 功能的开发者可能比较了解,可以采用 bundle 路径方式访问。如果不单独建立一个公共资源模块,则不需要考虑这么多。

小结

  • 对于使用目的明确的文件,比如以 .xcassets、 .xib 、 .storyboard 等为后缀的文件,不需要在 package.swift 中添加任何配置。
  • 对于用途不明确的文件,比如纯文本文件、脚本文件,则视情况在 package.swift 中使用不同属性配置(以下均是文件、文件夹均可配置):
    • 对于不需要被外部引用的,例如内部的开发者文档 README ,需要配置在 target.excludes 属性中。
    • 对于运行时有用到,可以被系统根据平台优化的文件,比如各种图片,需要配置在 target.resource.process 属性里
    • 对于运行时有用到,不存在优化的文件,比如各种图片,需要配置在 target.resource.copy 属性里

本地化

首先需要在配置文件中配置默认的语言:

let package = Package(
    name: "MyLibrary",
    defaultLocalization: "en",
    products: [

    ],
    dependencies: [

    ],
    targets: [

	]
)

然后根据你需要的语言创建对应的文件夹,文件名为对应的语言,后缀命名成 .lproj ,并在文件夹中创建 .strings 或者 .stringsdict 文件,如下图所示:

image.png

使用时:

	Button(action: roll, label: {   
                Text("Roll", bundle: Bundle.module)
                    .font(.title)
            })     

小结

本地化过程,首先需要在配置文件中声明默认语言,然后根据语言创建 .lproj 文件夹,再在文件夹里创建 .strings 或者 .stringsdict 文件,写上本地化的字符串。

需要注意的小点:

通过预处理命令区分编译环境

上文也说过,Package 可以通过 SwiftPM 执行 swift build 进行编译,也可以通过生成 xcodeproj 从而通过 Xcode 进行编译,两者的编译环境并不相同,生成的可执行文件也不是同一个地址,所以可以通过 SWIFT_PACKAGE 区分编译环境

#if SWIFT_PACKAGE
import Foundation
#endif

选择特定 Swift 版本的 Package

有条件的添加依赖

有些 Dependency 只希望在 Linux 环境下被依赖,其他环境下不被依赖。这个特性已经被提到了 这里,希望用如下的这种方式:

.package(url: "https://...", from: "1.0.0", when: .testing),
.package(url: "https://...", from: "2.0.0", when: .os(.linux),

Package.resolved

SwiftPM 也会生成一个 Package.resolved 文件来记录依赖项的解析结果,当执行依赖解析的时候,会优先解析这个文件,不存在时才会解析 Package.swift。
这一点和 Cocoapods 的 podfile.lock 文件类似,有的项目进行工程管理时为了能每个成员自由的执行 Update 操作,都会在上传时把它忽略掉。

添加资源文件

Swift 语言里的 PackageDescription 里(即 Swift 5.1)

  • 配置要求
    • Swift 5.3
    • Xcode 12

3. 开发编译测试

采用 Xcode 运行

打开Xcode, 默认 swift build 是不会生成 packageName.xcodeproj 这种 Xcode 可以直接打开的工程文件.
但是可以通过 swift package generate-xcodeproj 命令行生成一个 .xcodeproj 文件, 然后就可以通过 Xcode 运行该项目了,如果需要配置什么环境变量,则需要通过 Build Setting 中的选项配。

  • 需要注意的一点事,通过 swift run 和通过 Xcode 启动的是不同的进程,两种方式生成的可执行文件并不是同一个,所以如果需要把可执行文件更新到其他地方的时候注意别弄错了。

集成到工程中, 边开发边测试

    1. 执行swift package generate-xcodeproj命令,这里会生成Dependencies.xcodeproj
    1. 打开iOS工程,将第1步生成的project拖入工程作为sub-project,
    1. 添加依赖的framework
    1. 验证引入包是否成功

image.png

4. SPM包上传到云端

跟普通项目上传到第三方托管平台一样, SPM包上传到云端并不要想cocoapods一样执行相应的终端指令.

步骤:

    1. 代码编译通过没问题
    1. 上传到远程托管平台
    1. 打上tag(重要)

image.png

5. 在 Swift 项目中使用

  • 在 Xcode 导航 File -> Swift Packages -> Add Package Dependency...

image.png

  • 如下图:
      1. 通过仓库连接查找.
      1. 通过登录 git 账号查找

image.png

  • 指定仓库分支和版本号

image.png

  • 在 Project 中查看已经添加的spm包

image.png

  • 在代码 import spm包使用

SwiftPM 在 iOS 平台的使用

其实说 SPM 支持 iOS 等平台,个人觉得是有点问题的,因为这里只是 Xcode11 集成了 libSwiftPM,适配了 SPM 系统,从 SPM 本身的设计来看,并不能严格的说支持 iOS 等平台。

SwiftPM 对比 Cocoapods 和 Carthage

Cocoapods

使用最广泛的工具,依赖放在各个源(master 或者 自己的源)上的 podspec 文件进行下载代码库,在本地生成一个 workspace 进行统一管理、添加依赖。

  • 自动化/侵入性高:一键配置和集成依赖/自动更改 Xcode.project 的配置
  • 中心化:提供各个源管理仓库配置文件,所有更新仓库文件索引可能会很慢。
  • 缓存:除了项目根目录的缓存之外,还有较完整的本地缓存体系,所以不同工程下载同一个库时会直接从本地拿。

Carthage

  • 去中心化:没有统一管理的中心,所以没有更新中心服务器的文件索引这种耗时步骤
  • 成本略高/侵入性低:Carthage 只会帮你把各个库下载到本地(默认会编译成静态库),具体的 Project 配置需要自己弄
  • 生态环境:很差,很多库都没有提供这种依赖方式
  • 缓存:只有项目根目录的缓存,所以不同项目对于同一个库需要重新下载

SwiftPM

  • 去中心化:没有统一管理的中心,所以没有更新中心服务器的文件索引这种耗时步骤
  • 自动化/侵入性高:默认情况下需要有一定的目录格式
  • 生态环境:怎么说呢,不能说差,只能说不够成熟,还有很多待优化项,毕竟是官方开发,Xcode 自集成
  • 缓存:只有项目根目录的缓存,所以不同项目对于同一个库需要重新下载

参考