WWDC Quick Look 💓 By SwiftGGTeam
Swift packages: Resources and localization

Swift packages: Resources and localization

观看原视频

Highlight

Xcode 12 随 Swift tools version 5.3 为 Swift Package Manager 加入资源与本地化支持,让 package target 可以携带图片、storyboard、Core Data model、字符串文件等资源,并通过 Bundle.module 在运行时访问它们。

核心内容

Swift package 很适合把一段业务逻辑拆出去复用。问题出现在这段逻辑开始需要界面和资源的时候:一个骰子视图可以放进 package,按钮也能刷新点数,但图片资源不在 module 里,预览里只剩一个黑色方块。开发者要么把资源留在客户端 App,要么自己约定文件路径,复用边界很快变得脆弱。

Xcode 12 把这件事补上了。Package target 现在可以包含图片、storyboard、Core Data model 和其他运行时文件。文件类型清楚时,Xcode 会按后缀自动处理;文件用途含糊时,开发者在 Package.swift 里声明它应该被排除、处理,还是原样复制。

资源被构建成属于每个 code module 的 resource bundle。代码在运行时通过 Foundation 的 Bundle API 访问它,Swift 代码里最直接的入口是 Bundle.module。这个设计延续了系统已有 API,所以 package 增加资源不会提高 package 支持的最低 OS 版本。

本地化也进入同一套模型。Package manifest 声明 defaultLocalization,资源目录里放 en.lprojfr.lproj 这样的 localization directory,SwiftUI 和 UIKit 继续通过 bundle 参数寻找字符串和图片。这样,一个 package 提供的功能可以带着自己的默认语言、翻译文件和本地化资源一起分发。

详细内容

Package target 可以直接携带明确类型的资源

01:10)演示从一个 SwiftUI 骰子视图开始。Xcode 12 可以在没有客户端 App 的 workspace 里预览 package 内的 SwiftUI view。视图逻辑能运行,按钮能改变点数,缺的是显示点数的图片。

01:42)演讲者在 DiceUI target 旁边创建 asset catalog,并放入不同分辨率的骰子图片。因为 .xcassets 后缀有明确含义,Xcode 知道它应该在构建时被处理并打包进客户端产品。

关键 API 是给资源加载入口传入 module bundle:

Image("dice-one", bundle: .module)

关键点:

  • Image 仍然使用 SwiftUI 的图片加载入口。
  • "dice-one" 是 asset catalog 中的资源名,session 中的图片名称按点数匹配。
  • bundle: .module 指向当前 package module 的 resource bundle。
  • transcript 提到完整写法可以指定 module resource bundle,随后可借助 Swift 类型推断简写为 .module

manifest 负责说明含糊文件的用途

03:13)Xcode 会根据文件后缀判断一部分资源。Asset catalog、storyboard、Core Data model 这类文件有清楚用途,直接加入 target 即可。纯文本、普通图片、shell script 或任意目录的用途更含糊,它们可能是运行时资源,也可能只是开发资料。

04:09)官方代码片段展示了在 package manifest 里声明 target 文件意图的方式:

// swift-tools-version:5.3
import PackageDescription

let package = Package(name: "MyGame",
    products: [
        .library(name: "GameLogic",
            targets: ["GameLogic"])
    ],
    targets: [
        .target(name: "GameLogic",
            excludes: [
                "Internal Notes.txt",
                "Artwork Creation"],
            resources: [
                .process("Logo.png"),
                .copy("Game Data")]
        )
    ]
)

关键点:

  • // swift-tools-version:5.3 是资源支持的工具链边界。SwiftPM 的资源能力来自 Swift 5.3。
  • excludes 会让构建系统跳过内部笔记和设计素材目录。
  • .process("Logo.png") 表示这个文件需要作为资源处理。多数资源应使用 process,让构建系统按目标平台应用内置规则。
  • .copy("Game Data") 表示目录要原样复制,保留层级结构,适合运行时需要按目录访问的数据。

process 和 copy 的差别影响构建产物

06:56process 会对目录递归应用处理规则。Asset catalog 或 storyboard 会被转换成适合运行时使用的形式,图片也可能按平台规则被压缩。文件类型未知或不需要特殊处理时,最终也会被复制进产品。

07:35copy 的语义更直接:按字节复制,不做转换。session 给出的典型场景是保留整个目录结构,或者把本来可能被编译的源文件作为运行时模板使用。

这两个选择的关键差异可以简化成 manifest 决策:

.process("Logo.png")
.copy("Game Data")

关键点:

  • process 是默认优先考虑的资源动作,因为它允许 Xcode 使用平台内置规则。
  • copy 适合资源目录结构本身就是运行时协议的一部分。
  • 两者都属于 target 的 resources 数组,作用范围限定在声明它们的 package target。

Bundle.module 是 module 内部的资源入口

07:59)构建时,target 的源文件会编译成 code module,target 的资源会处理成该 module 对应的 resource bundle。Apple 平台上的 App 和 extension 本身是 bundle,package 的 resource bundle 会嵌入到 App bundle 中。命令行工具这类 unbundled product 需要把 resource bundle 安装在工具旁边。

09:38)运行时访问资源使用 Foundation 的 Bundle API。Xcode 为包含资源的 package target 合成 bundle accessor,Swift 和 Objective-C module 都能拿到对应 bundle。Swift 中的常用入口是:

let logoURL = Bundle.module.url(forResource: "Logo", withExtension: "png")

关键点:

  • Bundle.module 返回当前 code module 对应的 resource bundle。
  • url(forResource:withExtension:) 是现有 Foundation API,package 资源没有引入新的资源查找模型。
  • 这个 accessor 只对同一 module 内的代码可见。
  • 如果其他 module 需要资源,session 建议提供类型明确的 accessor,例如返回某张图片或某个数据文件,避免把整个 bundle 暴露成公共依赖。

本地化资源使用 defaultLocalization 和 .lproj

11:27)Xcode 12 也支持 package 内的 localized resources。第一步是在 manifest 里声明默认开发语言。这个语言会作为运行时没有更合适语言时的 fallback。

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "DiceUI",
    defaultLocalization: "en",
    targets: [
        .target(name: "DiceUI")
    ]
)

关键点:

  • defaultLocalization: "en" 声明 package 的默认本地化语言。
  • session 明确说明,包含资源的 package 需要声明默认 localization。
  • 语言 ID 使用 Apple 平台本地化中相同的语言标识规则。

12:02)接着创建 en.lprojfr.lproj,每个目录里放 Localizable.strings.lproj 后缀让 Xcode 明白这是 localization directory,所以通常不用在 manifest 的 resources 里单独列出。

Sources/DiceUI/en.lproj/Localizable.strings
Sources/DiceUI/fr.lproj/Localizable.strings

关键点:

  • Localizable.strings 保存 key 到目标语言文本的映射。
  • .stringsdict 也可以放在 localization directory 中,用于复数等规则。
  • localization directory 还可以包含某个语言专用的资源版本。
  • Asset catalog 自身也可以本地化,在不同语言中提供不同图片。

13:01)SwiftUI 文本同样通过 module bundle 取本地化字符串:

Text("roll", bundle: .module)

关键点:

  • "roll" 是本地化 key,session 中英语映射为 roll,法语映射为对应翻译。
  • bundle: .module 让 SwiftUI 到 package 的 resource bundle 中查找字符串。
  • SwiftUI preview 可以通过 environment 覆盖语言,session 用法语预览验证了 package 内字符串会生效。

核心启发

  • 做什么:把带 UI 的共享组件真正封装成 Swift package。为什么值得做:Xcode 12 可以让 package target 携带 asset catalog,SwiftUI preview 也能在没有客户端 App 的 workspace 里显示资源。怎么开始:把组件自己的图片放入 target 旁边的 asset catalog,加载图片时传入 bundle: .module

  • 做什么:把示例数据、模板文件或游戏关卡放进 package。为什么值得做:manifest 可以区分 .process.copy,运行时数据不用再依赖客户端 App 的目录约定。怎么开始:对需要平台处理的资源使用 .process(...),对必须保留目录层级的资源使用 .copy(...)

  • 做什么:为 package 提供窄口径资源访问 API。为什么值得做Bundle.module 只在同一 module 内可见,session 也建议不要把整个 bundle 暴露给其他 module。怎么开始:在 package 内部写 ImageProviderTemplateLoader 或类似类型,只返回调用方真正需要的图片、URL 或数据。

  • 做什么:让可复用组件自带本地化字符串。为什么值得做:package 可以声明 defaultLocalization,并携带 .lproj 下的 Localizable.strings.stringsdict怎么开始:先为默认语言创建 en.lproj/Localizable.strings,再添加目标语言目录,SwiftUI 文本使用 bundle: .module

  • 做什么:在 package 里预览不同语言的 UI。为什么值得做:session 演示了通过 SwiftUI preview 的 environment 覆盖语言,直接验证法语字符串来自 generated resource bundle。怎么开始:为关键 view 增加本地化 preview,覆盖 locale 后检查按钮文案、图片和布局是否仍然正确。

关联 Session

评论

GitHub Issues · utterances