Swift packages: Resources and localization
Developer Tools 进阶 26m

Swift Package 的资源管理与本地化

Swift packages: Resources and localization

2020年6月24日

在 Apple 官方观看视频

一句话判断

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
WWDC 2020