☰
SwiftGen 资源加载定制指南:用 `bundle` 与 `lookupFunction` 参数接管资源查找逻辑
2026/9/25 11:42:06 网站建设 项目流程
  • 开发工具
  • 代码生成

【免费下载链接】SwiftGen

The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!

项目地址:https://gitcode.com/gh_mirrors/sw/SwiftGen
点击查看免费下载

导读

SwiftGen 生成的代码默认从“生成代码所在的那个 Bundle”中加载资源——主 App 的代码从mainbundle 加载,Framework 的代码则从该 Framework 自己的 bundle 加载。这对大多数项目够用,但当你使用 CocoaPodsresource_bundles把资源打进独立子 bundle、需要运行时热更新翻译文件、或搭建多 white-label App 共用一个 Framework 的场景时,就需要更高级的方案。本文以 Customize-loading-of-resources.md 为主体,结合仓库内各模板的源码与测试,完整讲解如何通过bundle模板参数和lookupFunction模板参数接管资源查找逻辑,读完你可以在配置文件中精确指定“从哪里加载”乃至“如何加载”。

默认行为:代码在哪,资源就在哪

SwiftGen 内建模板默认通过一个名为BundleToken的私有类来定位资源 bundle。以 structured-swift5.stencil 为例,模板尾部会生成:

private final class BundleToken { static let bundle: Bundle = { #if SWIFT_PACKAGE return Bundle.module #else return Bundle(for: BundleToken.self) #endif }() }

这段代码的语义就是原文档开篇所述的两条默认规则:

  • 生成代码位于主 App target 时,Bundle(for: BundleToken.self)指向mainbundle;
  • 生成代码位于某个 Framework 时,它指向该 Framework 的 bundle;
  • 使用 Swift Package Manager 时,则回退为Bundle.module。

对大多数用户这已经够用。BundleToken在strings、fonts、files、ib、json、plist等几乎所有模板的实现细节段中都会按需生成,条件是“未提供bundle且未提供lookupFunction”。一旦你显式提供了这两个参数中的任意一个,BundleToken便不再生成。

场景一:CocoaPods Pod 的resource_bundles子 Bundle

问题背景

CocoaPods 官方建议用resource_bundles选项把 Pod 的资源打包进一个独立的 bundle(例如MyAwesomePodResources.bundle)。这样做的好处是:无论终端用户以何种方式集成你的 Pod,你的资源都不会与用户的资源冲突——这一点在静态链接时尤其重要,因为静态链接下 CocoaPods 不会为每个 Pod 生成 Framework,而是把所有内容塞进用户主 App 的 bundle。

代价则是:你的资源不再与生成代码位于同一 bundle,SwiftGen 生成的代码默认会加载失败。

解决:bundle模板参数

修复方法是在配置文件中为每个相关输出设置bundle参数,让它指向你代码中代表“资源 bundle”的那个表达式。原文档给出了一个典型的资源 bundle 定位器:

final class MyAwesomePod { // This is the bundle where your code resides in static let bundle = Bundle(for: MyAwesomePod.self) // Your resources bundle is inside that bundle static let resourcesBundle: Bundle = { guard let url = bundle.url(forResource: "MyAwesomePodResources", withExtension: "bundle"), let bundle = Bundle(url: url) else { fatalError("Can't find 'MyAwesomePodResources' bundle") } return bundle }() }

仓库中确实存在与之同构的测试辅助实现,见 Common-CustomBundle.swift:它同样通过Bundle(for:)获取自身所在 bundle,再在其中查找名为SwiftGenResources的子 bundle。这说明“Bundle(for:)+url(forResource:withExtension:)定位子 bundle”是官方测试环境验证过的标准写法。

随后在swiftgen.yml配置文件中,把bundle: MyAwesomePod.resourcesBundle加到需要该行为的模板输出下(原文档完整示例):

input_dir: Resources output_dir: Sources files: inputs: files/dir outputs: templateName: swift5 output: Generated/Files.swift params: bundle: MyAwesomePod.resourcesBundle fonts: inputs: Fonts outputs: templateName: swift5 output: Generated/Fonts.swift params: bundle: MyAwesomePod.resourcesBundle strings: inputs: en.lproj/Localizable.strings outputs: templateName: structured-swift5 output: Generated/Strings.swift params: bundle: MyAwesomePod.resourcesBundle

重新运行 SwiftGen 后,生成代码就会改为从MyAwesomePod.resourcesBundle加载本地化字符串与字体,而不再使用代码所在 bundle。

模板源码中bundle的落点

在 SwiftGen 的 Stencil 模板中,bundle参数会直接替换默认的BundleToken.bundle,替换点在各个模板的“Implementation Details”段:

  • strings 模板(structured-swift5.stencil):BundleToken.bundle被替换后调用bundle.localizedString(forKey:value:table:);
  • fonts 模板(swift5.stencil):bundle.url(forResource:withExtension:)用于定位字体文件 URL;
  • files 模板(flat-swift5.stencil):bundle.url(forResource:withExtension:subdirectory:localization:)用于定位普通文件;
  • ib 模板(scenes-swift5.stencil):NSStoryboard(name:bundle:)/UIStoryboard(name:bundle:)用于加载 storyboard;
  • json/plist 运行时模板(runtime-swift5.stencil):bundle.url(forResource:withExtension:)用于在运行时读取数据文件。

一个通用的表达是:bundle参数接受任何“能求值为Bundle的 Swift 表达式”,实际生效点取决于模板类型。

注意事项:模板支持范围

并非所有模板都支持bundle。XCAssets 模板(xcassets/swift5、xcassets/swift4)就不支持bundle/lookupFunction参数——因为资产目录根据资源类型(图片集、颜色集、数据集、符号等)会调用多种不同的加载方法,无法用单一 bundle 或单一查找函数统一接管。原文档也明确提示:请查阅 templates 文档 确认你要用的模板是否支持该参数。各模板的说明页(如 strings/structured-swift5.md)会列出参数表,其中bundle的默认值为BundleToken.bundle。

场景二:用lookupFunction完全接管查找逻辑

当“换一个 bundle”已经不够用时,可以进一步覆盖“lookup function”——即让 SwiftGen 生成的代码调用你自定义的查找函数,而不是直接访问某个 bundle。原文档给出三类典型动机:

  1. 应用内切换语言:想用 App 内自选的语言覆盖设备系统语言,重写本地化字符串的加载路径;
  2. 多 white-label App 共享 Framework:多个 App target 使用同一个 Framework,希望每个 App 能覆盖 Framework 提供的翻译;
  3. 运行时热更新翻译:App 定期检查并下载更新版本的翻译文件,本地嵌入的翻译作为兜底。

解决:lookupFunction模板参数

原文档的示例是一个TranslationService,它优先返回内存中更新的翻译表,否则回退到Bundle.main.localizedString(forKey:value:table:):

final class TranslationService { static let shared = TranslationService() private typealias TranslationTable = [String: String] private var updatedTables: [String: TranslationTable] = [:] func lookupTranslation(forKey key: String, inTable table: String) -> String { if let table = updatedTables[table], let translation = table[key] { return translation } else { return Bundle.main.localizedString(forKey: key, value: nil, table: table) } } }

(updatedTables如何从网络下载并更新,原文档明确留给读者自行实现。)

随后在配置文件中为strings输出添加lookupFunction参数,注意必须提供函数的完整签名(含外部参数名):

input_dir: Resources output_dir: Sources strings: inputs: en.lproj/Localizable.strings outputs: templateName: structured-swift5 output: Generated/Strings.swift params: lookupFunction: TranslationService.shared.lookupTranslation(forKey:inTable:)

生成代码中,tr(_:_:_:fallback:)的实现会从“bundle.localizedString”切换为调用你的函数,见 structured-swift5.stencil:

private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { let format = TranslationService.shared.lookupTranslation(forKey:inTable:)(key, table, value) // 等价于模板渲染结果:let format = TranslationService.shared.lookupTranslation(forKey:inTable:)(key, table, value) return String(format: format, locale: Locale.current, arguments: args) }

更精确地说,模板渲染后生成的是:

let format = TranslationService.shared.lookupTranslation(forKey:inTable:)(key, table, value)

即参数值(key、table、value)按位置传入你提供的函数引用。

lookupFunction的签名随模板而异

原文档特别强调:该函数签名取决于你使用的 parser 与模板,例如strings模板和fonts模板的要求就完全不同。仓库中各模板的文档页给出了精确签名:

模板类型要求签名示例
strings(flat/structured)(key: String, table: String, fallbackValue: String) -> StringyourFunctionName(forKey:table:fallbackValue:)
fonts(name: String, family: String, path: String) -> URL?myFontFinder(name:family:path:)
ib scenes(name: NSStoryboard.Name) -> NSStoryboard(macOS)
(name: String) -> UIStoryboard(其他平台)
myStoryboardFinder(name:)
json / plist runtime(path: String) -> URL?myJSONFinder(path:)

以上签名分别见 fonts/swift5.md、ib/scenes-swift5.md、json/runtime-swift5.md。

在 Stencil 源码中的实际调用点:

  • fonts 模板(swift5.stencil):return {{param.lookupFunction}}(name, family, path),用于计算字体文件 URL;
  • ib 模板(scenes-swift5.stencil):return {{param.lookupFunction}}(name),用于构造 Storyboard 实例;
  • json/plist runtime 模板(runtime-swift5.stencil):guard let url = {{param.lookupFunction}}(path),用于定位数据文件。

两个关键行为规则

  1. bundle与lookupFunction互斥:只要设置了lookupFunction,bundle参数就会被忽略(各模板文档的参数表均有此注明,见 strings/structured-swift5.md)。
  2. 命名参数必须提供完整签名:函数参数可以使用任意名称甚至省略外部名,但只要使用了命名参数,就必须在配置值中写出包含全部命名参数的完整签名,例如TranslationService.shared.lookupTranslation(forKey:inTable:)。这是模板把配置字符串直接拼进生成代码的必然要求——模板不会也无法替你推断签名。

测试中的实际用法:参数怎么传

仓库的模板测试展示了这两个参数的真实书写格式。以 FontsTests.swift 为例:

context: try StencilContext.enrich(context: context, parameters: ["bundle=ResourcesBundle.bundle"]) // 以及 parameters: ["lookupFunction=myFontFinder(name:family:path:)"]

也就是说,测试代码里传参格式是bundle=...与lookupFunction=...,与配置文件 YAML 中params:下的键值写法一一对应。而 Common-CustomBundle.swift 正是测试编译环境里用来模拟“Pod 资源子 bundle”的辅助类型——它验证了“Bundle(for:)+ 查找子 bundle”这种模式可以被 SwiftGen 生成的代码正常编译使用。

总结与使用建议

你的需求推荐参数说明
资源打进了独立子 bundle(CocoaPodsresource_bundles、手动打包等)bundle提供一个求值为Bundle的表达式即可,最简单直接
需要自定义加载逻辑(应用内换语言、热更新翻译、white-label 覆盖、自定义字体查找)lookupFunction提供完整函数签名,按模板要求匹配参数类型
两者同时出现—lookupFunction优先,bundle被忽略
XCAssets 模板均不支持资产目录按类型分派多种加载方法,无法用单一参数接管

动手之前,请先确认目标模板的参数支持情况——bundle/lookupFunction在 Documentation/templates 下每个模板的专属文档中都有明确参数表;生成后也建议像 TemplatesTests 那样用真实的资源 bundle 做一次编译验证,确保Bundle(for:)定位与url(forResource:)查找路径在目标平台(iOS/macOS/tvOS/watchOS、SPM 或 CocoaPods)上都正确无误。

  • 开发工具
  • 代码生成

【免费下载链接】SwiftGen

The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!

项目地址:https://gitcode.com/gh_mirrors/sw/SwiftGen
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询