Swift Package 的资源管理与本地化
Swift packages: Resources and localization
2020年6月24日
一句话判断
Swift Package 终于支持资源文件和本地化了——如果你之前因为”放不了图片和 XIB”而拒绝迁移到 SPM,现在最大的障碍已经消失了。
这场 Session 讲了什么
Swift Package Manager 在 Swift 5.3(Xcode 12)中引入了资源文件(Resources)和本地化(Localization)的原生支持。在此之前,Swift Package 只能包含 Swift 源码文件——不能放图片、JSON 配置、Core Data 模型文件,更不能做本地化。这迫使很多开发者继续使用 CocoaPods 或手动集成。
Session 详细介绍了 Package.swift 中新的 resources 声明语法。你可以使用 .process("Resources") 让 Xcode 自动处理资源文件,或者使用 .copy("Configs") 将文件原样复制到 bundle 中。前者会对 asset catalogs 和 .strings 文件做特殊处理,后者适用于需要保持原始目录结构的场景。
本地化方面,Swift Package 现在支持声明 defaultLocalization,并在 targets 中指定本地化资源的目录结构。String(localized:) API 可以直接在 Swift 代码中使用 .strings 和 .stringsdict 文件,也可以在 SwiftUI 中使用 Text("key") 的方式引用本地化字符串。
值得深挖的点
资源访问的 Bundle 机制
Swift Package 中的资源文件被打包到独立的 bundle 中,通过 Bundle.module 访问。这是一个由 SwiftPM 自动生成的静态属性,指向当前 package 的资源 bundle。这个设计有一个重要的隐含意义:如果你的 package 被多个 target 依赖,每个 target 会获得自己的 bundle 实例,资源不会冲突。但如果你在 package 外部代码中需要访问 package 的资源,需要通过 package 的 bundle 来定位。
.process() vs .copy() 的选择
.process() 会对资源文件做智能处理:Asset catalogs 会被编译,.strings 文件会被本地化处理,.xcstrings 会被编译为二进制格式。.copy() 则直接将文件复制到 bundle根目录,不做任何处理。Session 建议:对于需要编译处理的资源(asset catalog、storyboard、strings)用 .process();对于配置文件、数据文件、shader 代码等原始资源用 .copy()。
代码片段
// Package.swift 中声明资源和本地化
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyUIComponents",
defaultLocalization: "zh-Hans", // 默认中文简体
platforms: [.iOS(.v14)],
products: [
.library(name: "MyUIComponents", targets: ["MyUIComponents"]),
],
targets: [
.target(
name: "MyUIComponents",
dependencies: [],
resources: [
// 自动处理资源(编译 asset catalogs、strings 等)
.process("Resources/Images"),
// 原样复制配置文件
.copy("Resources/Config"),
// 处理本地化字符串
.process("Resources/Localizable.xcstrings"),
]
),
.testTarget(
name: "MyUIComponentsTests",
dependencies: ["MyUIComponents"],
resources: [
.copy("TestData")
]
),
]
)
// 在 Swift 代码中访问 Package 资源
import Foundation
public class ThemeManager {
// 使用 Bundle.module 访问 Package 内的资源
public func loadConfig() -> Config? {
// Bundle.module 由 SwiftPM 自动生成
guard let url = Bundle.module.url(
forResource: "theme", withExtension: "json",
subdirectory: "Config"
) else {
return nil
}
let data = try? Data(contentsOf: url)
return data.flatMap { try? JSONDecoder().decode(Config.self, from: $0) }
}
public func loadIcon(name: String) -> UIImage? {
// 从 Asset Catalog 加载图片
return UIImage(named: name, in: Bundle.module,
compatibleWith: nil)
}
}
// Swift Package 中的本地化
import Foundation
public struct LocalizedStrings {
// 使用 String(localized:) 引用本地化字符串
public static var welcomeMessage: String {
String(
localized: "welcome_message",
defaultValue: "欢迎使用",
bundle: .module,
comment: "应用启动后的欢迎语"
)
}
public static func itemCount(_ count: Int) -> String {
String(
localized: "item_count_\(count)",
defaultValue: "\(count) 个项目",
bundle: .module
)
}
}
// SwiftUI 中的本地化
import SwiftUI
public struct WelcomeView: View {
var body: some View {
VStack {
// SwiftUI 的 Text 自动支持本地化
Text("welcome_message", bundle: .module)
.font(.title)
// 使用插值的本地化
Text("item_count_\(5)", bundle: .module)
.foregroundColor(.secondary)
}
}
}
最佳实践
- 在
Package.swift中声明defaultLocalization,即使暂时只有一种语言——未来添加多语言时会少很多麻烦 - 使用
.process()处理所有需要编译的资源类型(Asset Catalog、Storyboard、Strings),用.copy()处理原始数据文件 - 在 package 的公共 API 中不要暴露
Bundle.module,而是在内部封装好资源访问逻辑 - 本地化字符串文件统一使用
.xcstrings格式(Xcode 15+),它比.strings更容易管理 - 测试中使用
Bundle.module访问测试数据,不要把测试数据放在源码目录中
还有什么值得关注
- Session 提到
Bundle.module的自动生成是通过编译器在 Derived Data 中创建一个resource_bundle_accessor.swift文件实现的 - 如果 package 中没有任何资源文件,
Bundle.module不会生成,引用会导致编译错误 - 资源文件会增加 package 的体积,建议只包含必要的资源,大文件考虑使用 on-demand resources