Swift Package Manager工程实践

Swift Package Manager工程实践
GuoYanjun- 文/一月筠
-- 转载请注明 --
- 目录…
Swift Package Manager
(下文简称 SwiftPM
)是苹果官方提供的一个用于管理源代码分发的工具,它与Swift构建系统集成在一起,可以自动执行依赖项的下载,编译和链接过程。该工具可以帮助我们编译和链接 Swift packages(包),管理依赖关系、版本控制,以及支持灵活分发和协作(公开、私有、团队共享)等。支持Swift、Objective-C、Objective-C ++、C或C ++。 SwiftPM
包管理器支持 macOS 和 Linux,与CocoaPods和Carthage功能类似,不过比这两个更简洁,代码的侵入性更小,也不需要额外安装工具。
本文将详细介绍团队在引入SwiftPM
进行工程实践中,探索和累积的相关知识和实践经验,我们将从结构设计、资源处理、链接方式的选择、编译与链接参数设置、异常处理,这五个方面展开详细介绍,每个小部分结尾都提供了最佳实践的总结;希望能够帮助其他想要尝试SwiftPM
的开发者顺利过渡;
本文面向了解SwiftPM
基本知识,但是没有深度使用的开发者;如果你还不了解SwiftPM
,建议首先阅读WWDC相关视频;
结构设计
梳理清楚代码之间的依赖关系,对于设计SwiftPM
中,模块如何组成和进行合理拆分,非常重要!
代码组织方式
- 建议从两个维度考虑组件的组织方式,组件的性质:基础组件,业务组件;代码的依赖关系:通用组件,专用组件;
- 优先统一代码语言,把Objc代码全部转成Swift后,会很方便整合到
SwiftPM
中; - 如果Objc代码过多,或者这部分OBjc代码是一个完整的功能模块,即不存在Swift/Objc互相依赖混编的情况,也可以拆分到一个
SwiftPM
中; - 如果一个模块依赖另一个模块中的极少数代码,可以考虑复制所需代码到本模块,并标记为非
public
,解除模块之间耦合关系;
目录结构
推荐使用命令行方式创建SwiftPM
;注意命名时尽可能与模块功能相符,不要包含Lib、Framework等不能体现功能的描述;
创建SwiftPM
1 | $ mkdir MyPackage |
默认创建的SwiftPM
项目的名字就是文件夹的名字;这将创建带有目标target和相应测试target的库,包含所需的目录结构和单元测试,如下面所示:
1 | Tests //单元测试 |
组织结构与路径设置
生成的Package.swift
如下面所示:
1 | let package = Package( |
由于一个SwiftPM
库包可以包含多个target
,所以Sources/Tests
目录下存放源代码的目录结构组织方式,有不同的选择;有两种方式可供参考:
- 单Target模式,默认情况下,SwiftPM自动创建生成对应的target目录,如当前目录结构所示,查找Sources下MyPackage目录,**
Sources
目录下仅有一个目录MyPackage
,且该目录名与target的name参数保持一致**;当需要通过多个目录结构拆分代码时,只能在MyPackage
下创建子目录,不能与MyPackage
同级; - 多Target模式,在
Sources
目录下,创建多个平级目录,如targetA,targetB,同时在Package.swift
中targets数组中对应进行配置,必要时指明target的path路径;
接下来我们通过一个示例,展示多Target模式,目录结构如下所示,
1 | //... |
多Target模式Package.swift
示例代码如下所示,
1 | let package = Package( |
其中需要特别提到target对象中path参数的设置
1 | static func target( |
当target的name参数指定的字符串与对应Sources下的目录名称完全一致时,则无需设置path参数,SwiftPM
默认查找name参数同名目录;
不符合上述情况时,则必须指定path参数;
- 如果name参数名与Sources下目录名不一致,则需要指定path参数路径;
- 如果所有文件直接放在Sources下,没有新建文件夹,则需要指定path参数为
path: "Sources"
; - path参数设置时,支持相对路径模式,即可以使用“.”或“..”路径匹配。
依赖处理
SwiftPM
类似CocoaPods
,可以添加其他依赖的Package,这里以添加本地依赖项为例说明:
1 | let package = Package( |
- Package顶层的dependencies,添加的是外部依赖;外部依赖是指当前Package以外其它
SwiftPM
; - 每个target中的dependencies添加的是当前target需要的依赖项;可以是外部依赖,在数组中增加外部依赖的名称;可以是其他的target,使用
.target
方式引入;
1 | .target(name: "TargetA", |
这样TargetA中可以调用MyPackage中对外提供的API接口;
1 | import MyPackage |
Objc与Swift混编
在SwiftPM
中,一个target中,只能存在一种语言,不可以混编;
假设现在有一个完整的业务功能的代码,是混编的,即Swift/Objc代码都有;我们的目标是使用SwiftPM
进行模块化处理;一种简单方案是将混编代码中的Objc代码,先转写成Swift代码,这样就不再存在混编问题,下一步处理成SwiftPM
是最简单的;
但是,可能由于一些限制或者原因,Objc不能变成Swift;必须混编。
两种改成SwiftPM
的解决方案:
- 多Package模式:将代码按语言不同分开,拆分成两个Package,但是需要满足不存在循环依赖关系,只是单向依赖没有问题;
- 单Package模式:在一个Package中,
Sources
文件夹下按语言建立两个单独的目录,分别存放Swift代码和Objc代码,在Package配置文件中建立两个Target,每个Target设置明确的path路径,如path: "Sources/A"
,同时设置Target的依赖关系;
注意:任何一种方式都需要满足模块代码之间不能循环依赖,如果有这种情况发生,需要先重构代码!
最佳实践
- 梳理清楚代码之间的依赖关系,对于设计
SwiftPM
目录结构至关重要; - 根据实际情况,选择“单target模式”或“多target模式”组织代码目录结构;
- 当target的name参数指定的字符串与对应Sources下的目录名称完全一致时,无需设置path参数,其他情况下需要设置;
- 处理Objc与Swift混编时,需要满足模块代码之间不存在循环依赖关系;
资源处理
在开发过程中,图片、文本、JSON、XML等资源文件,是我们必须使用的;接下来通过一个具体的实例,展示不同类型的资源文件SwiftPM
如何处理;这里创建了一个实例工程SpmResourceTest
,列举了各种资源文件的存放方式和目录设置;
Package内部几种资源处理的方式
- 直接把资源文件拖到项目中:放在根目录下,如
ic_linkfailed_mid_normal@2x
图片,或者放在创建的子目录下,如images目录和json目录,下面都有不同的资源文件; - 使用
Asset Catalog
,在SwiftPM
中会默认命名为Media - 使用
Bundle
,如HYContentShare.bundle
从DerivedData
找到对应的项目编译后的成果

注意到出现了一个SpmResourceTest_SpmResourceTest.bundle
的生成文件,显示包内容查看内部结构:

当前target的resources设置
1 | .target( |
发现一些不同的规律:
- 没有在配置中明确指定的文件,如
ic_linkfailed_mid_normal@2x
是不会被处理的,即不被编译进当前bundle中;在编译时,编译器也会出现提醒,例如found 3 file(s) which are unhandled; explicitly declare them as resources or exclude from the target
; - 使用
procees
处理的文件目录,其目录结构下的内容会被 平铺(减少目录结构层级的一种方式) 放到bundle的根层级下,没有子目录出现,如images目录; - 使用
copy
处理的文件目录,会保留目录层级结构,放在bundle中,如json目录; - 资源中如果包含bundle文件,如当前例子中HYContentShare.bundle,不同
swift-tools-version
版本使用有区别;swift-tools-version:5.5
及以下,使用copy
与process
没有区别,都会保持HYContentShare.bundle
结构;swift-tools-version:5.6
及以上,如果使用copy
处理,保持HYContentShare.bundle结构;如果改为process
处理,则也会将资源平铺放到根目录下,不再有HYContentShare.bundle;如下图所示;

注意:由于
Swift Package Manager
随着时间推移也在不断迭代,所以需要注意swift-tools-version
中指定的版本号,有些功能在高版本与低版本内部实现有差别,某些新功能在高版本才能使用;// swift-tools-version:5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
读取资源
下面介绍读取资源的代码实现;
读取Asset Catalog中图片
1 | //从Media直接获得图片,不需要区分2x 3x |
获取prcoess处理的资源
图片
1
2
3
4
5
6
7
8
9
10
11//方式1,通过路径path
public func getImageUrl() -> UIImage{
let url = Bundle.module.url(forResource: "ic_right@2x", withExtension: "png")
let path = url?.path ?? ""
let image = UIImage(contentsOfFile: path)
return image!
}
//方式2,使用UIImage的API
public func getImageUrl2() -> UIImage{
UIImage(named: "ic_right", in: .module, compatibleWith: UITraitCollection())!
}其他资源
仍然通过Bundle.module.url
获得path1
2let url = Bundle.module.url(forResource: "xzloading_middle", withExtension: "json")
let path = url?.path ?? ""
获取copy处理的资源
1 | //创建获取Bundle路径的扩展函数 |
最佳实践
- 优先使用
Asset Catalog
管理资源,使用起来最简便; - 如果不能使用Asset,比如json文件或者plist,并且可能混合图片资源一起使用(如Lottie动画),优先使用
process
处理; - 如果需要多套同名资源同时存在,如
dark/icon.png
,light/icon.png
,则通过建立多个目录,使用copy
处理是合适的,使用时读取不同的path;
链接方式的选择
静态链接与动态链接的区别
我们在编写代码的同时,也需要使用别人提供的库或者框架,就需要使用链接器;链接分为两种类型:
- 静态链接,它发生在编译构建 App 的时候,影响到构建的耗时以及 App 最终的二进制体积;
- 动态链接,它发生在 App 启动的时候,影响 App 的启动耗时;
由于上述两种链接方式的区别,一般来说,推荐更多的使用静态链接,减少动态链接,来降低App启动耗时;但是,如果是在开发/调试阶段,频繁修改的代码,建议采用动态链接方式,降低构建的耗时。
SwiftPM编译链接选项
1 | static func library( |
其中type参数的类型是Product.Library.LibraryType
,可以通过指定.static
或.dynamic
来决定生成的SwiftPM
最终产物的形态,是动态链接库或是静态链接库;
通过下面这张图,解释一下不同的type参数设置,对生成的framework的影响:
- 我们假设有两个
SwiftPM
,分别为consumer
和producer
,其中consumer
依赖producer
; - 每个
SwiftPM
在编译后都会生成对应的中间文件,可重定位对象文件(relocatable object
),即.o
文件; - 当
consumer
和producer
的type均设置为nil
或static
时,都会仅生成中间文件.o; - 当
consumer
的type设置为dynamic
,producer
的type设置为nil
或static
,在生成对应的中间文件后,consumer.o
与producer.o
会进行合并,生成最终的动态链接库consumer.framework
- 当
consumer
的type设置为dynamic
,producer
的type设置为synamic
,在生成对应的中间文件后,consumer.o
与producer.o
会分别生成对应的动态链接库consumer.framework
与producer.framework
通过上面的举例描述,我们可以得到下面结论:
SwiftPM
的LibraryType设置为dynamic
时,会生成动态链接库,同时当前库所依赖的库,如果不是设置为dynamic
,则会被合并编译成一个动态链接库framework;
最佳实践
- 如果
SwiftPM
的产品可以静态链接,也可以动态链接。建议不要明确声明库的类型,这样SwiftPM
可以根据包使用者的偏好,选择静态链接还是动态链接。
编译与链接参数设置
定制编译参数
在CocoaPods使用中可以通过配置指定在Debug模式下导入;
1 | pod 'SourceModel', :configurations => ['Debug'] |
对应的,SwiftPM
也可以通过配置实现相同的功能,并且功能更强大;我们通过一段代码示例来展示一下,这段代码源自官方Swift提案;
1 | import PackageDescription |
关闭ARC
我们在项目中一直使用Google版本的protocolbuffer作为埋点数据序列化工具;到目前为止,它仍然仅支持Objective-C,不支持Swift;
生成的代码是非ARC的,需要特别在BuildSetting中标注;
如果对代码进行改造使用SwiftPM
包管理器进行处理,需要设置参数关闭ARC;
1 | .target( |
注意:这种方式针对当前target进行设置,与原有的BuildSetting中针对文件的设置不同,影响target中所有文件;
预编译宏设置
我们继续通过Google版本的protocolbuffer为例分析,通过proto文件生成的Objc头文件包含预编译宏,如下面所示:
1 |
参考CocoaPods配置
1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 $(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 |
在taregt的cSettings
中需要设置宏,才能编译通过;
1 | .define("GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS") |
但是,问题并没有真的解决,当其他模块依赖当前target时,公开暴露出去的 header 的 #if 判断不能使用任何自定义的宏变量,所以均无法编译通过;
因此,在SwiftPM
中不能通过任何自定义的宏变量的方式,向外暴露头文件;
链接参数设置
通过前面的分析可以发现,由于Swift Package Manager发展时间相对CocoaPods更短,很多三方组件或开源项目均没有适配SwiftPM,无对应的Package.swift配置,所以当我们项目中自行接入此类项目时,参考对应项目的CocoaPods配置是很好的借鉴方式;
还以高德地图为例,对应项目的CocoaPods配置如下所示:
1 | OTHER_LDFLAGS = $(inherited) -l"c++" -l"z" -framework "CoreLocation" -framework "CoreTelephony" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" |
这里我们借鉴上面的配置,在当前target的linkerSettings中增加必要的链接参数;
1 | linkerSettings: [.linkedLibrary("c++"), |
一般来说,通过这种方式调整后,都可以顺利编译通过,正常使用。
异常处理
接下来介绍一些使用SwiftPM
可能会遇到的特殊情况,如何处理。
非Clang Module生成的SwiftPM接入
Clang Module
包含module.modulemap
,某些三方SDK历史版本比较老,生成的XCFramework不含module.modulemap
,在通过SwiftPM
接入项目主工程时,例如MAMapKit(高德地图),这类模块无法直接暴露给Swift,必须通过桥接方式引入;
1 | #import <MAMapKit/MAMapKit.h> //高德地图SDK需要保留,MAMapKit没有被编译为Mudule,Swift引用不到,必须通过桥接方式使用 |
无法解决的包管理的问题
如果使用Xcode + SwiftPM 遇到奇怪问题,使用过了Clean Build Folder
,删除DerivedData
,重启Xcode等一系列方式后,仍然无法解决,请尝试下面方式;
1 | rm -rf ~/Library/Caches/org.swift.swiftpm |
总结
Swift Package Manager
作为苹果推出的包管理依赖工具,可以说是补足了苹果生态的短板;SwfitPM
相比Cocoapods
它配置更加简洁易用,相比Carthage
更加轻量化,无入侵;因为Swift本身是跨平台语言,SwfitPM
完全使用Swift编写,所以使用场景不仅仅局限于Mac平台;
随着今年WWDC22上Swift Package Plugins
的发布,解决了SwfitPM
目前不支持在构建期间执行任何自定义操作的问题,包括源代码生成以及特殊类型资源的自定义处理等问题;为提高研发流程效率,更加自动化提供了有力的支持;