概览
Swift Package Manager(SPM)是 Xcode 内置的包管理工具,支持远程公/私有库和本地库。
创建 Package Manager
创建方法
两种方法:
- 在 Xcode 菜单栏依次选中 File > New > Package Manager
- 在目标文件夹中使用命令:
Swift package init
创建完成后,在 Sources 文件下添加代码,然后按 cmd + B 编译。如果发现编译器报错,是因为测试代码有误。如果我们不需要编写测试代码,注释即可。
目录结构
如下是一个 Package 的目录结构:
. ├── Package.swift // 配置文件 ├── README.md // 包的功能、使用说明 ├── Sources // 源码目录 │ └── Biu │ └── Print.swift └── Tests // 测试文件目录 ├── BiuTests │ ├── BiuTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift
Package.swift 包含如下内容:
需要特别注意在Package.swift中指定 platforms,也就是这个Package能够支持的平台。
// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( // 包名 name: "Biu", // 需要注意Package运行的平台, 否则代码可能编译不过去!!!!! platforms: [ .iOS(.v14), ], // 包对外提供的 products(库、可执行文件) products: [ .library( name: "Biu", targets: ["Biu"]), ], // 依赖的其它包 dependencies: [ // .package(url: /* package url */, from: "1.0.0"), ], // 包含的 targets targets: [ // 每个 target 所需依赖 .target( name: "Biu", dependencies: []), .testTarget( name: "BiuTests", dependencies: ["Biu"]), ] )
name: 包的项目名称 platforms: 支持的平台及对应平台的最低版本 targets: 包含多个target的集合,我们指定target的名字为ZZPackage,xcode会自动把Sources/ZZPackage目录下的所有文件添加到package中。如果你想再新建一个target, 需要在Sources/目录下新建一个文件夹,然后再targets数组中添加新的target。 .target是PackageDescription.Target实例类,参数说明: name: target名字 dependencies: target的依赖,主要指定Package添加的依赖module的名字 path: target的路径,如果自定义文件夹需要设置此参数 exclude: target path中不希望被包含的path sources: 资源文件路径 publicHeadersPath: 公共header文件路径 products: 对外公开导出target产物,使得其他target能够使用它们。如果不写会编译报错 .library( name: "ZZPackage", type: .static, targets: ["ZZPackage"]) : 可指定静态库或动态库,默认静态库 dependencies: 添加包所依赖的其他第三方package包的集合 .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"), .package(url: "https://github.com/SnapKit/SnapKit.git", from: .init(5, 0, 1)), .package(url: "https://github.com/SnapKit/SnapKit.git", from: .init(stringLiteral: "5.0.1")), .package(url: "https://github.com/SnapKit/SnapKit.git", Package.Dependency.Requirement.branch("master")), : 指定分支,如果第三方不支持spm的话可以使用这种方式 .package(path: "../ZZPackage") : 关联本地的SPM库 swiftLanguageVersions: 支持的swift版本
本地添加和测试 Package
- 新建一个 Biu_test 工程,直接将 Biu 目录拖入工程
- General -> Frameworks, Libraries, and Embedded Content 下添加 Package
- 导入 Package(
import Biu
)即可
注:如遇到无法导入或找不到 Package 的问题,可尝试退出工程或重启 Xcode 解决 🤷♂️
发布与更新 Package
可以使用 Git 命令,也可以使用 Xcode 内置的版本控制工具。
初次发布时,初始化 Package 为 Git 仓库,打上 Tag,再推送至远程仓库。
更新 Package 后,打上新的 Tag,Push 即可。
使用 Package
添加
我们先移除上文中的工程 Biu_test 引入的本地 Package(Biu),然后在 Xcode 中选择 File > Swift Packages > Add Package Dependency…,输入 Package 地址(https://github.com/keisme/Biu),点击 Next 安装。
安装完成后,重新运行项目,结果与预期一致。
更新
如果需要更新 Package,选择 File > Swfit Packages > Update to Latest Package Versions。
移除
在工程中找到 Swift Package Manager,移除相应的 Package。
Package的例子
File > New > Package Manager 创建一个TestPackage
创建一个SwiftUIView类
我们把TestPackage上传到Github上去。
新建一个Xcode工程–>点击File–> Add Packages
右上角输入github的url -> 选择Add Github Enterprise account。
输入Account 和 Token。
Token的获取方法,Setting -> Developer settings
Personal access tokens –> Generate new token
用Token登录后,可以看到这个TestPackage 点击Add Package添加到工程
在DemoApp中我们可以看到可以引用到 TestPackage中的 SwiftUIView。
Swift Package Manager 添加资源文件
从更新到现在,SwiftPM 令人诟病的一个问题就是无法在包里添加资源文件。这对于已经习惯于使用 CocoaPods 的开发者造成了很大的麻烦,当然目前 SwiftPM 差于 Cocoapods 不止这一点。SwiftPM 也意识到了这一点,从去年就可以看到 github 的 SwiftPM 对应仓库的有 resource
等 API 相关提交。
此次的 SwiftPM 更新中除了上面说的可以添加资源文件,还添加了本地化等功能。
SwiftPM 的资源文件管理功能在 swift-tool-version 5.3,即 Swift 5.3,对应 Xcode 12。所以 package.swift
配置中需要声明 swift 5.3 以上(这行并不是注释,而是文件解析中必须的配置):
添加和配置资源文件
对于一些使用目的明确的文件类型,比如下面图中的这些。开发者不需要在 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、
对于SwiftUI来说,可以直接通过Bundle.module 来访问,图片放到Target下面的的Media.xcassets即可
struct HeaderBackgroundView: View { var body: some View { GeometryReader { gr in ZStack { Image("imageName", bundle: Bundle.module).resizable().frame(width: gr.size.width, height: gr.size.height * 0.26) }.frame(width: gr.size.width, height: gr.size.height * 0.26) } } }
题外话:在SwiftUI中读取指定Bundle中图片的例子
import SwiftUI struct ContentView: View { var body: some View { Group{ if let resBundlePath = Bundle.main.path(forResource: "Resources", ofType: "bundle"), let resBundle = Bundle(path: resBundlePath), let uiImage = UIImage(named: "imagename", in: resBundle, with: nil){ Image(uiImage: uiImage) .resizable() .scaledToFit() }else{ Color.red } } } }
Package中开发SwiftUI画面
在Package中开发SwiftUI画面有些尴尬,始终没找到运行的方法,只能先通过 SwiftUIView_Previews来调试了。