iOS开发Swift

Swift Package Manager的一些Tips

概览

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.xcassetmain.storyboard 文件,Xcode 能明确知道它代表什么,所以不需要在这个配置文件中标记
  • internal Note.txt 文件和 Artwork Creation 文件夹是模块内部文件,所以写在 targetexclude 属性中,这样 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来调试了。