Swift Package Manager 使用
参考资料
- Swift 包管理器介绍
- Swift Package Manager 添加资源文件
- Swift Package Manager使用总结
- 使用 Xcode 创建独立 Swift 软件包
- 苹果开发者简体中文文档
- 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 代表用户界面文件等
而对于一些使用目的不太明确的文件类型(如下图中的一些文件类型),则需要在 package.swift 文件中配置。例如纯文本文件,这种文件中的数据可能是需要在运行时被加载而计算或者展示,也可能只是一个开发者文档。
对于上面这种意义不明的文件,就需要在 package.swift 清单中根据规则配置,下面以这个 GameLogin 作为例子:
- 对于 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 中。
在 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
由于 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 文件,如下图所示:
使用时:
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 可以直接打开的工程文件.
但是可以通过
<font color="red">swift package generate-xcodeproj</font>
命令行生成一个 .xcodeproj 文件, 然后就可以通过 Xcode 运行该项目了,如果需要配置什么环境变量,则需要通过 Build Setting 中的选项配。
- 需要注意的一点事,通过 swift run 和通过 Xcode 启动的是不同的进程,两种方式生成的可执行文件并不是同一个,所以如果需要把可执行文件更新到其他地方的时候注意别弄错了。
集成到工程中, 边开发边测试
- 执行swift package generate-xcodeproj命令,这里会生成Dependencies.xcodeproj
- 打开iOS工程,将第1步生成的project拖入工程作为sub-project,
- 添加依赖的framework
- 验证引入包是否成功
4. SPM包上传到云端
跟普通项目上传到第三方托管平台一样, SPM包上传到云端并不要想cocoapods一样执行相应的终端指令.
步骤:
- 代码编译通过没问题
- 上传到远程托管平台
- 打上tag(重要)
5. 在 Swift 项目中使用
- 在 Xcode 导航 File -> Swift Packages -> Add Package Dependency...
- 如下图:
- 通过仓库连接查找.
- 通过登录 git 账号查找
- 指定仓库分支和版本号
- 在 Project 中查看已经添加的spm包
- 在代码 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 自集成
- 缓存:只有项目根目录的缓存,所以不同项目对于同一个库需要重新下载