- 开发工具
- 构建工具
【免费下载链接】swift-package-manager
The Package Manager for the Swift Programming Language
本篇技术指南围绕 Swift Package Manager(SwiftPM)的资源(Resources)捆绑机制展开,适用于所有需要在 Swift 包中随源码分发静态资源(资产目录、测试夹具、属性列表、图片、本地化文本等)的开发者。读完本文,你将掌握swift-tools-version: 5.3及以上版本的资源声明语法、process/copy两种核心规则与embedInCode扩展规则的区别、exclude排除机制,以及如何通过Bundle.module在代码中安全访问资源,并理解资源捆绑在 SwiftPM 内部的实现原理。
什么是 Swift 包资源捆绑
在 Swift 5.3 之前,Swift Package Manager 构建的包只能包含源码,无法把图片、plist、测试数据等文件随包一起分发。从swift-tools-version: 5.3开始,SwiftPM 引入了资源捆绑(resource bundling)能力:只要在Package.swift中声明// swift-tools-version: 5.3或更高版本,就可以把非源文件作为资源与源码一起打进包里。
典型可捆绑的资源包括:
- 资产目录(asset catalog)、XIB、storyboard、Core Data 数据模型等 Apple 平台资源;
- 测试夹具(test fixtures)、示例数据文件;
- 属性列表(plist)、JSON 配置文件;
- 图片、音频等媒体文件;
- 本地化资源(localized resources)。
其核心设计原则是资源与目标(target)强绑定:资源作用域限定在所属 target 内,与源码一样必须放在该 target 对应的目录中,无法跨 target 共享。
添加资源文件:目录约定与放置规范
SwiftPM 把目标 sources 目录中发现的非源文件一律视为该目标的"资产"(asset),并自动纳入资源处理流程。例如,MyLibrary目标的所有资源默认位于Sources/MyLibrary目录下。
为了清晰地区分源码与资源,官方文档与 SwiftPM 都推荐为资源单独建立子目录。最常见的做法是建立一个名为Resources的目录,使所有资源文件位于Sources/MyLibrary/Resources下。这一约定在 Runtimes/PackageDescription/Resource.swift 的类型注释中也有明确说明:使用子目录组织资源文件,可以简化文件识别与管理。
目录结构示意:
MyPackage/ ├── Package.swift └── Sources/ └── MyLibrary/ ├── MyLibrary.swift └── Resources/ ├── text.txt ├── settings.plist └── data/ └── fixture.json需要强调的是,资源被严格限定在所属 target 内部,因此不要把资源文件放在 target 目录之外,否则 SwiftPM 无法将其归属到任何 target。
显式声明资源:process 与 copy 规则
并非所有资源都会被编译器自动处理。对于编译器无法自动识别的文件类型(例如普通的图片文件),必须在包清单中将其显式声明为资源;而在 Xcode 中构建时,Xcode 会自动处理若干常见资源类型(如 XIB、storyboard、asset catalog 等),这些无需在清单中声明。
process 规则:首选的处理方式
对于绝大多数使用场景,官方推荐使用process(_:localization:)规则。它请求编译器根据目标平台对该类型资源应用已知的处理流程。例如在支持图片优化的平台上,Xcode 可能对图片做针对性优化;如果没有可用的特殊处理,编译器会将该资源原样拷贝到资源包的顶层目录。process规则作用于目录路径时,会递归地应用于目录内的所有文件。
在Package.swift中显式声明一个资源文件的完整示例:
// swift-tools-version: 5.3 import PackageDescription let package = Package( name: "MyPackage", targets: [ .target( name: "MyLibrary", resources: [ .process("Resources/text.txt") ] ), ] )上面的示例将Sources/MyLibrary/Resources/text.txt声明为MyLibrary目标的资源。
copy 规则:保留原样与目录结构
某些 Swift 包要求资源文件保持原样不被处理,或必须保留特定的目录结构(例如插件模板、脚本资产)。此时应使用copy(_:)规则:
- 传入文件路径时,文件被原样拷贝到资源包的顶层目录;
- 传入目录路径时,编译器会保留该目录的完整结构。
这正好与process规则形成对照:process处理目录时会递归展开,而copy处理目录时会保留目录层级。
embedInCode 规则:把资源编译进可执行代码
除文档重点讲解的process与copy外,从 Swift 5.9(@available(_PackageDescription, introduced: 5.9),见 Runtimes/PackageDescription/Resource.swift)开始,SwiftPM 还提供了第三种规则embedInCode(_:):将资源文件的字节内容直接嵌入可执行代码,生成一个PackageResources结构体,其中每个嵌入资源对应一个以资源名命名的静态属性(点号替换为下划线)。例如嵌入内容为Hello Swift的identifier.txt,生成的代码等价于:
struct PackageResources { static let identifier_txt: [UInt8] = [72,101,108,108,111,32,83,119,105,102,116,10] }embedInCode适合希望在运行时彻底摆脱文件系统依赖、把数据直接放进二进制镜像的场景。
三种规则的 API 定义与参数
在 Runtimes/PackageDescription/Resource.swift 中,Resource的公开 API 如下:
| API | 引入版本 | 说明 |
|---|---|---|
process(_ path: String, localization: Localization? = nil) | 5.3 | 按平台应用已知处理,无处理则原样拷贝;目录递归处理 |
copy(_ path: String) | 5.3 | 原样拷贝到资源包顶层;目录保留结构 |
embedInCode(_ path: String) | 5.9 | 将资源字节嵌入可执行代码 |
其中process的localization参数接受Localization枚举,取值为.default(默认本地化)与.base(基础国际化)。
资源在包内的落点:destination 计算
在 SwiftPM 内部(Sources/PackageModel/Resource.swift),每个资源由Resource结构体描述,包含rule(规则)与path(路径)两个字段,并通过destination计算属性确定其在资源包中的相对位置:
- 使用
process且指定了非Base的localization时,资源落在<localization>.lproj/<文件名>路径下(localizationDirectoryExtension常量即"lproj",且局部化标识会被统一转为小写); - 其他情况(
process无本地化、copy、embedInCode)下,资源落在资源包顶层,仅保留文件名。
Resource.Rule枚举在内部共有三个 case:process(localization: String?)、copy、embedInCode,与 PackageDescription API 一一对应。值得一提的是,process本地化目录还支持以Base命名的目录用于基础国际化。
排除资源:exclude 参数与警告机制
如果一个文件位于 target 的目录内,但你不希望它成为包资源,可以把它传给 target 初始化器的exclude参数。例如,Sources/MyLibrary/instructions.md仅用于本地文档、不应被打包:
targets: [ .target( name: "MyLibrary", exclude: ["instructions.md"] ), ]关于exclude,官方文档给出如下实践建议,并有源码层面的印证:
- 尽量避免把非资源文件放进 target 的 sources 目录;
- 如果不可避免,不要逐个文件地排除,而是把要排除的所有文件集中到一个目录,再把目录路径加入
exclude数组; - SwiftPM 会对 target 的
Sources目录中无法识别的文件发出警告。
在实现层面,Sources/PackageLoading/TargetSourcesBuilder.swift 会将target.exclude解析为绝对路径集合excludedPaths,在遍历 target 目录时跳过这些路径(见excludedPaths相关的文件扫描逻辑),并对无效的 exclude 路径、重复的源文件声明以及未处理的资源分别发出诊断警告(例如"Found unhandled resource at ...")。
此外,Runtimes/PackageDescription/Target.swift 明确指出:exclude中的路径相对于 target 路径,且该参数优先于sources与resources参数——也就是说,被排除的路径不会同时被当作源文件或资源处理。
在代码中访问资源:Bundle.module
当一个 target 包含资源时,编译器会为该模块自动创建一个资源包(resource bundle),并生成一个对Bundle的内部静态扩展,用于定位包内资源。在代码中使用该扩展即可访问资源,无需关心资源的实际物理位置。
例如,读取随包分发的属性列表settings.plist的 URL:
let settingsURL = Bundle.module.url(forResource: "settings", withExtension: "plist")重要提示:访问资源时必须始终使用
Bundle.module。Swift 包不应假设资源的准确存放位置——不同平台、不同构建方式下资源包的物理路径可能不同,只有通过Bundle.module才能获得 SwiftPM 保证的正确路径。
Bundle.module 是如何生成的
从源码可以确认Bundle.module并非手写代码,而是 SwiftPM 在构建期生成的。在 Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift 中:
needsResourceBundle检查目标资源中是否存在非embedInCode规则的资源——只要存在,就需要生成资源包;bundlePath根据 target 的potentialBundleName与构建参数中的bundlePath(named:)计算资源包的最终路径;- 构建阶段会生成名为
resource_bundle_accessor.swift的访问器源码(generateResourceAccessor()方法),其中定义Bundle.module静态属性; - 同时还会生成资源包的
Info.plist(对应 Sources/Build/BuildPlan/BuildPlan.swift 中的generateResourceInfoPlist)。
也就是说,Bundle.module会在每个包含资源的模块中自动可用,开发者直接使用即可。
向依赖方暴露资源
如果希望把某个包资源提供给依赖该 Swift 包的 App 使用,可以为它声明一个public 常量。例如,向使用本包的应用暴露settings.plist的 URL:
public let settingsURL = Bundle.module.url(forResource: "settings", withExtension: "plist")通过公开 API 暴露资源,可以让外部消费者通过稳定的符号访问资源,而无需关心资源包内部的具体路径。
资源捆绑的完整实战示例
仓库中的 Fixtures/Resources/Simple 是 SwiftPM 自身的测试夹具,展示了资源捆绑的真实用法。其 Package.swift 声明了swift-tools-version:5.3,并在多个 target 中使用copy规则捆绑foo.txt:
// swift-tools-version:5.3 import PackageDescription let package = Package( name: "Resources", targets: [ .target( name: "SwiftyResource", resources: [ .copy("foo.txt"), ] ), // SeaResource、ClangResource、CPPResource、MixedClangResource 同理 .testTarget( name: "ClangResourceTests", dependencies: ["ClangResource"] ), ] )对应的目录结构(节选):
Fixtures/Resources/Simple/ ├── Package.swift ├── Sources/ │ ├── SwiftyResource/ │ │ ├── foo.txt │ │ └── main.swift │ ├── ClangResource/ │ │ ├── include/Package.h │ │ ├── Package.m │ │ └── foo.txt │ └── ... └── Tests/ └── ClangResourceTests/ └── ClangResourceTests.m从这个示例可以看出两点实践:
- 资源文件直接放在 target 源码目录下即可被识别,无需
Resources子目录(子目录只是推荐的组织方式); - 资源声明对 Swift、Clang(C/Objective-C)、C++ 等各类 target 均适用,且测试 target 可以依赖携带资源的 target。
工作原理小结与最佳实践
把以上内容串联起来,SwiftPM 资源捆绑的完整工作流为:
- 放置:将资源文件放入 target 的 sources 目录(推荐
Sources/<TargetName>/Resources子目录); - 声明:在
Package.swift的 target 初始化器中用.process(...)(首选)、.copy(...)(需保留原样/结构)或.embedInCode(...)(Swift 5.9+,嵌入代码)显式声明,或用exclude排除不应打包的文件; - 构建:SwiftPM 构建资源包、生成
Info.plist与resource_bundle_accessor.swift(内含Bundle.module); - 访问:代码内通过
Bundle.module.url(forResource:withExtension:)等 API 访问资源;需要对外暴露时,通过 public 常量发布。
实用建议汇总:
- 始终使用
Bundle.module定位资源,不要硬编码路径; - 能用
process就用process,让平台有机会优化资源(如图片压缩),仅在需要保持字节原样或保留目录层级时使用copy; - 用
Resources子目录组织资源,保持源码目录整洁; - 把临时文件、文档等非资源文件统一放进一个目录并用
exclude排除整个目录,避免逐个文件排除和产生警告; - 资源作用域限定在 target 内,跨 target 共享资源没有官方支持路径,应通过目标依赖(dependency)间接使用携带资源的库。
如果你希望进一步深入,可以继续阅读仓库中的相关实现:Sources/PackageModel/Resource.swift(内部资源模型)、Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift(资源包与访问器生成)、Sources/PackageLoading/TargetSourcesBuilder.swift(资源扫描与警告),以及测试夹具 Fixtures/Resources/Simple。
- 开发工具
- 构建工具
【免费下载链接】swift-package-manager
The Package Manager for the Swift Programming Language
相关推荐
Swift Package Manager 资源支持(SE-0271)完全指南:从 manifest 声明到 `Bundle.module` 运行时访问
Swift Package Manager 资源支持(SE 0271)完全指南:从 manifest 声明到 Bundle.module 运行时访问 本篇技术指
文档Swift Package Manager 资源管理实战:Bundle、Localized 资源与插件生成资源的正确姿势
Swift Package Manager 资源管理实战:Bundle、Localized 资源与插件生成资源的正确姿势 本文是 Swift Package M
开发工具构建工具如何从源码构建 Swift Package Manager:贡献者开发环境搭建完整指南
如何从源码构建 Swift Package Manager:贡献者开发环境搭建完整指南 如果你想在本地 从源码构建 Swift Package Manager
开发工具构建工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考