一、研究背景
blog.cocoapods.org/CocoaPods-S…
Cocoapods官宣不再进行功能上的更新,整个项目转为维护状态。Xcode工程的第三方组件依赖和tag更新目前都依赖于Cocoapods,对于苹果官方的SPM并没有对应的支持和脚本进行CI/CD支持,为了应对Cocoapods不再更新的问题,目前需要将组件打包、发版、更新等业务进行SPM(swift package manager)迁移。
Swift Package Manager(以下简称SPM)是苹果在swift3.0中加入的一个包管理工具,用于处理模块代码的下载、编译和依赖关系等。跟CocoaPods和Carthage功能类似,不过比这两个更简洁,代码的侵入性更小,也不需要额外安装工具。
二、研究细节
SPM集成之前,先要了解一下 SPM是什么以及其工作的原理
官方介绍:
The Swift Package Manager is a tool for managing distribution of source code, aimed at making it easy to share your code and reuse others’ code. The tool directly addresses the challenges of compiling and linking Swift packages, managing dependencies, versioning, and supporting flexible distribution and collaboration models.
直译就是:
Swift Package Manager 是一个用于管理源代码分发的工具,旨在轻松共享您的代码并重用其他人的代码。 该工具直接解决了编译和链接 Swift 包、管理依赖项、版本控制以及支持灵活的分发和协作模型的挑战。
1. 基本概念
1.Package
package是SPM管理的基本单位,包含对应的源码、资源、依赖以及相关的配置信息,每个package通常对应一个独立的功能模块或者库
2.Target
target是Package中的一个构建单元,可以是一个库,可执行文件等,一个package可以包含多个target
3.Dependency
Dependency指package所依赖的其他package,SPM会自动下载并解析这些依赖,确保项目的所有库都能正确构建。
4.Product
在包中的每个target最终都可能构建成一个Library或者一个execute作为product,这是package编译后的产物,
Xcode构建流程
SPM的优点
对比Cocoapods,SPM具有以下优点。
- 无需安装,Xcode11以上版本自带
- 苹果官方维护,不用担心和Cocoapods一样停止维护
- 安装第三方库的时候比Cocoapods快(依赖源在github,有些要翻墙)
- 使用SPM构建时比Cocoapods快
SPM缺点
- 每次打开App 都会重新拉取 所有依赖的库
- 更新时间长(访问github 还需要进行科学上网)
- 支持文档少,
- 远端仓库对网络要求高
简单对比一下SPM、Cocoapods和Carthage的差异
Cocoapods | Carthage | SPM | |
---|---|---|---|
原理 | Cocoapods会将所有的依赖库都放到另一个名为Pods的项目中,然后让主项目依赖Pods项目 | 自动将第三方框架变成Dynamic framework | swift构建系统集成在一起,可以自动执行依赖项的下载、编译和链接过程 |
支持语言 | Swift OC | Swift OC | swift OC |
是否兼容 | 兼容Carthage SPM | 兼容 Cocoapods SPM | 兼容Cocoapods Carthage |
支持库数量 | 多 | 少于Cocoapods | 少于Cocoapods |
使用配置复杂度 | 中 | 高 | 低 |
项目入侵性 | 严重入侵 | 没有侵入性 | 没有侵入性 |
项目编译速度 | 慢 | 中 | 快 |
源码可见 | 可见 | 不可见 | 可见 |
2.创建swift Package
创建swift Package有两种方式
1.使用命令行的方式
mkdir SwiftPackageTest # 生成的Package的名称
cd SwiftPackageTest
swift package init --type executable # executable是可执行文件 library生成framework
type参数说明 --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 已经被其他方式取代)
文件结构说明:
- Package.swift:描述当前Package的信息及其依赖
- main.swift:文件在Source目录下,代表整个命令行工具的入口,文件名不要进行替换
- 单元测试目标位于名为Tests的文件中
2.使用Xcode 界面工具
创建的Library的目录结构(创建过程中 可以选择是否需要集成Test)
同样 由于选中了Test,生成了Tests对应的模块便于测试
3.Package配置
let package = Package(
name: "swiftPackageTest",
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, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "swiftPackageTest"),
]
)
为了更好的说明每个key值对应的 对package中对应字段进行说明
-
name: Swift包的名字,或者‘ nil ’使用包的Git URL来推断名字。
-
defaultLocalization:资源的默认本地化。
-
platforms:具有自定义部署目标的受支持平台列表。
- 支持的平台和对应的系统版本
- platforms:[
- .macOS(.v11), .iOS(.v12),.tvOS(.v12)
- ]
-
pkgConfig: C模块的名称。如果存在,Swift包管理器会搜索 <名称>。获取系统目标所需的附加标志。
-
providers:系统目标的包提供程序。
-
products:此包提供给客户使用的产品列表。
- 编译后的产物一般分为两种 可执行文件 静态库或动态库
-
dependencies:包依赖列表。
-
添加依赖的包,一般指向包源的git路径和版本环境,或者包依赖的本地路径
-
依赖包的添加支持以下五种方式
- 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"),
-
-
-
targets:作为这个包的一部分的目标列表。
-
target是Package的基本构建,和xcodeproject一样,Package可以有多个target
-
target分为三种类型
- 常规性 .regular
- 测试类型 .test
- 系统库类型 .system
-
-
swiftLanguageModes:此包兼容的Swift语言模式列表。
-
linkerSettings: 依赖的系统的库 比如
z
,sqlite3
,SystemConfiguration
等系统类库 -
clanguagstandard:用于此包中所有C目标的C语言标准。
-
cxxlanguagstandard:用于此包中所有c++目标的c++语言标准。
4.资源文件配置
对于Xcode中的一些明确的文件类型,比如下面的文件类型,开发者不需要在Package.swift文件中进行配置,Xcode知道其用途。
对于使用目的不太明确的文件类型,需要在Package.swift中配置规则,如下图
-
对于media.xcassets和main.storyboard文件,Xcode能明确知道他代表什么,不需要再进行配置
-
Internal Note.txt和Artwork creation文件夹是模块内部文件,所以写在target的exclude属性中,这样Xcode 就不会把它编译到包里
-
其他不能自动识别的类型并且需要被加载到package里的文件则配置在resource属性中,其中resources有两个静态方法
-
process() 苹果推荐方式,配置的文件会根据具体使用的平台和内置规则机型适当的优化,对storyboard和asset catalog转为适当的形式,包括图片压缩等。若文件无法处理 只是简单的文件拷贝也就是copy()
-
copy() 简单的进行拷贝
-
5.混编问题
Creating a standalone Swift package with Xcode | Apple Developer Documentation
官方文档中支持多个语言进行混编,但是其中有一段是对混编的限制,SPM中有多个Target,同一个Target中不能进行混编,即swift文件不能和Objc文件放到同一个target中
To add source files to a Swift package, use workflows that you already know. For example, you can add a source file to a package by dragging it into the Project navigator, or by using the File > Add Files to [packageName] menu. Targets can contain Swift, Objective-C/C++, or C/C++ code, but an individual target can’t mix Swift with C-family languages. For example, a Swift package can have two targets, one that contains Objective-C, Objective-C++, and C code, and a second one that contains Swift code.
6.创建demo进行测试
根据上面的方法直接生成一个library,
创建完成后,当前版本是swift的,
1.Swift package 支持Objective-C
上面说了 SPM支持Objective-C/C++, or C/C++,但是这些不能和swift文件在一个target中,需要我们后期对这个进行处理,
- 删除除了Package.swift 文件的其他文件,创建多个OC文件,目录如下
在Package.swift中设置好 products和target,发布该Package到gitlab或github中,通过add dependency添加到项目中,首次设置gitlab时需要设置accessToken
- 随便创建一个工程集成刚才创建的package
按照上述的步骤 就可以集成一个package到项目中
2.Swift package 同时支持swift 和 Objective-C
按照上面的规则 我们需要在Package.swift文件中分别申明一个swift和OC的target,如下图所示
-
分别创建swift和OC两个target,swift target可以依赖OC对应的target,
-
OC工程中通过SPM集成该package时,需要调用swift中方法时,swift中写法要遵循一定的规则才能在OC项目与中进行访问
- 供OC调用的类必须继承至NSObject, 类前需要用@objc进行修饰,对于文件中的访问权限必须设置为open/public
- 多个需要公开的属性 可以使用
@objcMembers
进行批量设置 - 方法必须声明open/public的权限,且用@objc进行修饰
3.Swift package 默认支持swift库
在此不再赘述
4.库中添加依赖
创建新的类库,需要依赖其他的基础库完成对应的业务,需要在当前package.swift中进行对应的配置,在上面说过配置dependency大概有以下几种写法
- git源 + 确定的版本号
- git源 + 版本区间
- git源 + commit号
- git源 + 分支名
- 本地路径
如上图,在实际开发过程中只会使用 确定版本、指定分支以及本地的加载方式进行开发,其他可以了解一下.
5.jpg,svga,webp等资源加载
- 资源文件添加
- 资源文件访问
SPM中对于特定的资源类型文件不需要加载,Xcode知晓其用途,但是其他的资源文件就需要我们在SPM中进行设置。项目中需要配置通用的svga和webp文件,根据上面资源文件配置的说明对package.swift做一下修改。
- 单个target资源使用
dependencies: [
/// 只使用 4.0.0版本
.package(url: "https://github.com/AFNetworking/AFNetworking.git", exact: "4.0.0")
],
targets: [
.target(
name: "TestDYSPMTool",
//可以不用配置,如果想在swift中调用OC的需要配置
dependencies: ["TestDYSPMTool_Objc"],
path: "TestDYSPMTool",
resources: [
//设置对应的资源文件,配置资源文件
.process("Resources/Images"),
.process("Resources/Animations")
],
swiftSettings: [ .define("SPM_MODE")]
),
.target(
name: "TestDYSPMTool_Objc",
path: "TestDYSPMTool_Objc",
publicHeadersPath: ""
)
]
左边是在当前package的目录下新增了一个Resources文件夹,对应的有Images和Animations两个文件夹,配置对应的资源就可以在工程中进行使用,我们在工程中通常有以下两种配置
- 资源文件在 .xcassets中
- 大图png资源在单独文件中
以swiftUI为例,讲解以上两种方式的使用区别
- 资源文件在 .xcassets中通过
Bundle.module
来访问包内的资源,需要指明bundle
- 资源文件在文件夹中
不仅要指明加载的bundle,还需要通过Image(uiImage: image)
的方式进行加载,在swift或OC中 都可以通过这种方式来加载,在此不在过多赘述。
- 多个target共用一套公共资源
package内部支持多个target共存,最常见的就是同时支持swift和OC的package,此时package内部需要对swift和OC对应的target分别设置一套公共资源,按照方式一的方式,每个target中都要配置一次,不仅会增大package的体积,也不利于后期对公共资源的维护。
对于这种情况可以在package中多创建一个target,该target只是一个公共资源文件的集合,内部可以定义便利方法提供给其他target或主工程使用。
在文件接口和package配置中一共定义了三个target,
- 资源文件单独创建一个target,对外提供便利访问方法,
- Objective-c target: OC对应的配置target,依赖于资源文件target
访问资源
- swift target: swift对应的配置target,依赖于资源文件target
这样在OCTarget和SwiftTarget中 就可以直接访问资源Target中的公共资源文件,
- 主工程中对package中的资源进行访问
通过package中的bundle,可以在工程中任何地方访问到对应的资源文件。
6.静态库处理
对于第三方的类库,使用cocoapods时一般会将其封装成一个独立的静态库,只暴露对应的头文件。在Swift package中也可以通过类似的方式进行设置。接下来 用声网第三方sdk库做示例。在官网下载sdk,构建对应的swift package,文件结构如下
- 配置package.swift
其中BinaryTargetLib中是声网对应的所有framework的集合,对package.swift文件进行设置
let package = Package(
name: "BinaryTargetPackage",
products: [
.library(
name: "BinaryTargetPackage",
targets: ["BinaryTargetPackage"]),
],
targets: [
.target(
name: "BinaryTargetPackage",
dependencies: [
"AgoraAIDenoiseExtension",
"AgoraCore",
"Agorafdkaac",
"AgoraFullAudioFormatExtension",
"AgoraRtcKit",
"AgoraSoundTouch",
"AgoraSpatialAudioExtension"
]
),
//加载framework 指定本地路径或者创建url 以zip包的方式进行加载
.binaryTarget(name: "AgoraAIDenoiseExtension",path: "BinaryTargetLib/AgoraAIDenoiseExtension.xcframework"),
.binaryTarget(name: "AgoraCore",path: "BinaryTargetLib/AgoraCore.xcframework"),
.binaryTarget(name: "Agorafdkaac",path: "BinaryTargetLib/Agorafdkaac.xcframework"),
.binaryTarget(name: "AgoraFullAudioFormatExtension",path: "BinaryTargetLib/AgoraFullAudioFormatExtension.xcframework"),
.binaryTarget(name: "AgoraRtcKit",path: "BinaryTargetLib/AgoraRtcKit.xcframework"),
.binaryTarget(name: "AgoraSoundTouch",path: "BinaryTargetLib/AgoraSoundTouch.xcframework"),
.binaryTarget(name: "AgoraSpatialAudioExtension",path: "BinaryTargetLib/AgoraSpatialAudioExtension.xcframework")
]
)
BinaryTargetPackage
只是作为一个对应package name,可以根据业务将声网相关的API调用封装在BinaryTargetPackage
中,提供给其他package使用,方便后续做替换和修改。
- 主工程加载
在主工程加载BinaryTargetPackage
,可以看到对应的声网framework也全部进行了加载。
- 主工程中进行调用
上面只是对framework进行封装的演示,一般在项目中 不会直接调用sdk,都会创建一个 专门的manager来对三方sdk进行调用。
- 对.a 静态库不支持
同样的按照上面的方式,创建二进制库,使用同样的方式来集成.a
二进制库目前只支持url zip包,xcframework以及artifacbundle的文件类型进行加载
7.现有OC类库支持处理
由于历史原因,现有的大部分底层组件和业务组件都是OC,使用Cocoapods进行管理。未来版本迭代中由于Cocoapods不再更新新功能和Swift Package Manager日益完善,终要从Cocoapods过渡到SPM。但是现在项目中面临如下问题
- 项目中集成了RN,RN目前还不支持SPM集成(待RN官方支持)
- 项目中使用了.a等静态库,需要转成framework的方式
- 其他(待更新)
当然 抛去上面的问题对业务组件和第三方SDK做一个简单的封装就可以用SPM的方式来进行加载,接下来 我们用简单的方式对现有的组件进行改造,使其支持SPM。
Reachability组件是纯OC组件,其中文件只有下图的两个文件,
在原有的项目基础上对文件接口做修改,将上面的两个文件放在Reachability
文件夹中
设置path和publicHedersPath,linker其依赖的系统库,当然在podspec文件中 也要修改source_file文件的加载路径。在测试工程中进行集成,能正常运行
上述修改比较简单直接,使组件库同时支持SPM和Cocoapods,对于复杂依赖和linker多个系统库的组件需要花费时间整理package.swift文件,规划路径和依赖。
总而言之,想要百十个组件同时支持SPM和Cocoapods是一个艰巨的工程,需要花费大量时间和精力。每个组件的依赖和配置都因业务而异,脚本只能只能处理大部分基本的工作,资源的加载、系统库linker、framework的封装、.a静态库的处理等都是需要精细处理的。
8.多package 本地修改工程配置
由于涉及组件过多,不可能通过GUI界面的方式手动的一个一个的进行添加,在业务开发中需要对加载的方式进行修改比如 本地加载 -> 分支加载 -> 指定版本加载,需要在这三种方式中进行切换,需要通过脚本来对配置文件进行统一的修改,目前研究下 有以下两种方式。
方式一:将主工程当做一个Module Library
- 主工程中创建一个 Package.swift文件本身作为一个module
在当前工程目录下执行以下脚本,会自动生成Package.swift文件
swift package init --type library
2. 双击Package.swift文件 添加依赖
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TestSwiftPackage",
defaultLocalization: "zh-Hans",
platforms: [ .iOS(.v13) ],
dependencies: [
///设置依赖 package
.package(url: "https://github.com/AFNetworking/AFNetworking.git", exact: "4.0.1"),
.package(url: "https://github.com/jdg/MBProgressHUD.git", exact: "1.2.0"),
.package(url: "https://github.com/SnapKit/SnapKit.git", exact: "5.7.1"),
.package(path: "../BinaryTargetPackage"),
.package(path: "../Reachability")
],
targets: [
.target(
name: "TestSwiftPackage",
path: "TestSwiftPackage"
)
]
)
由于是在主工程中进行设置,不需要生成对应的Library对外使用,product对应的直接省略,主要在dependencies字段中进行添加、删除和修改操作,打开Package.swift文件会自动拉取dependencies中配置的package
配合使用下面的命令可以快速更新
swift package resolve
swift package update
swift package clean
方式二:通过脚本批量在主工程中添加package
- 创建一个swift 可执行脚本
mkdir AddDependencyLib
cd AddDependencyLib
swift package init --type executable
2. 打开Package.swift文件,添加 XcodeProj依赖
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "AddDepencyLib",
platforms: [ .macOS(.v13) ],
dependencies: [
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "8.24.8")
],
targets: [
.executableTarget(
name: "AddDepencyLib",
dependencies: [
.product(name: "XcodeProj", package: "XcodeProj")
]
),
]
)
3. 编写配置文件 demo中简单配置
本地生成一个 Packages.swift文件类似于Cocoapods中的Profile文件,对要依赖的第三库做配置, 完全自定义格式,简单配置package的地址、 路径、版本号、加载方式等
/// type 0 表示使用 tag 1 使用分支 2 本地加载
let packages = [ ["name":"AFNetworking","url":"https://github.com/AFNetworking/AFNetworking.git","version":"4.0.1","type":"0"],
["name":"MBProgressHUD","url":"https://github.com/jdg/MBProgressHUD.git","version":"1.2.0","type":"0"],
["name":"SnapKit","url":"https://github.com/SnapKit/SnapKit.git","version":"5.7.1","type":"0"],
["name":"BinaryTargetPackage","url":"../BinaryTargetPackage","version":"","type":"2"]
]
4. 编写脚本读取对应的配置文件
在第一步中的依赖是添加和修改xcodeproj的工具,将抽象的.xcodeproj
配置文件使用swift的方式定义,方便在脚本中修改setting rule中的值。添加仓库的关键操作
// The Swift Programming Language
// https://docs.swift.org/swift-book
//编写对应的脚本
import Foundation
import XcodeProj
import PathKit
//读取文件路径 加载依赖包
let arguments = CommandLine.arguments
let path = arguments[1]
………………………………………………………………
/// 添加对应的依赖
@MainActor func addDepencyToProj(_ package: PackageInfoModel, rootObject: PBXProject){
if package.type == "0" {
if !rootObject.remotePackages.contains(where: { $0.repositoryURL == package.url }) {
do{
_ = try rootObject.addSwiftPackage(repositoryURL: package.url, productName: package.name, versionRequirement: .exact(package.version), targetName: "TestSwiftPackage")
print("添加远端仓库成功===\(package.name)")
} catch {
print("添加远端仓库失败\(package.name),error:\(error.localizedDescription)")
}
}
} else if package.type == "1" {
//分支加载
if !rootObject.remotePackages.contains(where: { $0.repositoryURL == package.url && $0.versionRequirement == .branch(package.version) }) {
do{
_ = try rootObject.addSwiftPackage(repositoryURL: package.url, productName: package.name, versionRequirement: .branch(""), targetName: "TestSwiftPackage")
print("添加远端仓库成功===\(package.name)")
} catch {
print("添加远端仓库失败\(package.name),error:\(error.localizedDescription)")
}
}
} else if package.type == "2" {
//本地加载
//该判断不准确,通过xcodeProj添加的本地类库 并不在 localPackages中
//rootObject.localPackages.contains(where: { $0.relativePath == package.url })
if let dependencies = localDependencies[rootObject.name] {
if !dependencies.contains(where: { $0 == package.name }) {
do {
_ = try rootObject.addLocalSwiftPackage(path:Path(package.url), productName: package.name, targetName: "TestSwiftPackage")
print("添加本地仓库成功===\(package.name)")
} catch {
print("添加本地仓库失败\(package.name),error:(error.localizedDescription)")
}
}
}
}
}
执行上面脚本时,方便测试,参数路径是写死的,后期可以根据项目的路径来配置
按上面的脚本简单执行效果如下
添加的远端和本地的类库不在一个位置,这就导致了使用rootObject.localPackages判断是否添加本地类库时总是false,会导致多次添加,这个还需要在研究,在git上也给作者提了issues 等待作者回复
脚本完善之后 执行以下命令直接生成一个可执行文件
swift build -c release
#当然有多个pruduct 还需要指明对应的product
将上面生成的脚本分发给组内成员 就可以执行swift package 集成流程,使用者不需要知道其内部实现,涉及的到命令有如下(简单罗列)
init
install
update
clean
……………… 跟Pod使用类似
另外可以在[组件化工具(内部使用)]中将其封装为GUI界面工具,使用方式和以前一致,组内成员可以无缝接入
*对比: *方式对比差异
方式一: 本身就是一个Library,设置各种denpendency即可,根据业务按着不同方式加载对应的module,
- 优点:方便操作,维护成本小,
- 缺点: 每次都需要双击Package.swift 打开项目依赖,之前的工程的并没有任何依赖的痕迹,对项目侵入性大
方式二:自定义程度高,需要自己编写脚本分发给组内成员,配置一次即可 操作上繁琐, 需要熟悉自定义命令和操作流程
-
优点:完全自定义,打开方式便捷,对项目侵入性小,便于以后的CI/CD集成
-
缺点: 需要花费时间编写脚本,成员需要熟悉编辑命令 类似于Pod的操作
最后发现在配置中每次update或resolve之后,link Library都会增加,
需要在脚本中手动清除所有link Binary
9.package发布编译验证和tag 发布
package发布时需要依赖持续集成(CI)工具进行自动构建和测试,通过之后在对package 执行add tag操作
swift package clean
swift build -c release #直接编译 默认是debug 指定release 编译
swift package dump-package # 检查包描述文件的正确性
git tag -a "版本号"
这些都是在本地执行,进行的本地验证流程,工程中我们使用gitlab,可以借助gitlab中的CI/CD 功能在每次merge时执行自动构建 测试等任务
-
使用genkins,配置执行脚本先验证package是否编译通过,再修改 tag
-
使用gitlab CI/CD 功能 在每次merge时执行 管道任务 执行成功修改tag
10.版本依赖冲突问题(待解决)
由于在package Dependencies中必须对其依赖的其他package指定一个精确的版本或者是一个范围的版本,不像Cocoapods中 s.dependency '``DYFoundation``'
其实际使用的版本是主工程来决定的,但是在swift package中在其Package.swift 中必须指定一个具体的或有范围的版本号,如下图。这时候swift package 会根据主工程的package依赖的版本对依赖包中的版本做约束(使用from或 range方式),但是这个约束会跟随版本的迭代慢慢的变成冲突,这个还需要在后续的业务中进行优化和处理,保证主工程和依赖中的package使用的是同一个版本。
11.指定条件编译
在使用Cocoapods时,我们可以通过下面的方式指定加载的组件,该方式根据指定的条件加载组件,
pod 'xxxxx', '1.0.0', :configurations => ['Release']
pod 'xxxxx', '1.0.0', :configurations => ['Debug']
但是在SPM中并不支持条件化加载组件,所有依赖的组件都会加载到工程中,如果不加区分,会增大包体积,一些测试代码也会编译到正式包中,目前只能通过其他的方式进行设置。会增加集成成本和复杂度。
实现方案一: 可参考 LookinServer
的实现方式,将整个文件内容都包裹在一个宏中,Debug下定义该宏,Release模式下不定义,这样在Release模式下,就相当于加载空文件 什么都不做
12.设置工程中的宏
Cocoapods中可以同时设置工程和组件中的宏,在组件编译和工程运行的时候都不需要在进行设置,但是在swift package manager中并不支持在主工程直接设置,如下面的设置,自定义宏GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
,在pod install
时会把宏添加到主工程中。
s.user_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1' }
s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1' }
但是在swift package manager中只在组件中进行了宏的设置,并不会在组件中进行宏的设置,这就导致了组件能编译通过,而在主工程编译失败或者加载的路径不同。
后期可在脚本中统一进行配置,针对不同的target 配置不同的宏
13.版本号设置问题
在 Swift Package Manager (SPM) 中,版本号必须符合 语义版本控制(Semantic Versioning,SemVer) 的标准。根据 SemVer 规则,版本号的格式应该是 MAJOR.MINOR.PATCH
,每个部分都应该是非负整数。这意味着像 1.0.3.1
这样的版本字符串是不合法的,因为它有四个部分。
合法的版本示例
1.1.0
是合法(其中 major = 1,minor = 1,patch = 0)1.2.3
是合法0.1.0
是合法1.0.0-alpha
是合法(包含预发布标签)
Cocoapods中可以使用四位版本号,但是在SPM中只能使用三位
三、未来建议
由于Cocoapods的停止更新新功能的通知,迁移SPM也是时间长短的问题,目前阶段第三方类库的SPM的支持率还不是很高,尤其是一些OC类库,在通常开发中需要对百十个组件进行配置来支持SPM,涉及到各种类型
-
纯底层或业务类型,不涉及资源
-
二进制类型,对.a和.framework 静态库的支持不是很友好
-
C/C++类型的类型 需要特殊配置
-
第三方SDK 需要对其进行封装
-
业务组件,带有额外资源配置
-
混编类型组件,SPM中不支持 swift和OC混编,需要特殊处理
-
RN组件不支持SPM,需要等待官方支持后更新
-
…………
四、参考资料
Creating a standalone Swift package with Xcode | Apple Developer Documentation