- 开发工具
- 代码生成
【免费下载链接】SwiftGen
The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!
导读
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。原文档给出三类典型动机:
- 应用内切换语言:想用 App 内自选的语言覆盖设备系统语言,重写本地化字符串的加载路径;
- 多 white-label App 共享 Framework:多个 App target 使用同一个 Framework,希望每个 App 能覆盖 Framework 提供的翻译;
- 运行时热更新翻译: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) -> String | yourFunctionName(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),用于定位数据文件。
两个关键行为规则
bundle与lookupFunction互斥:只要设置了lookupFunction,bundle参数就会被忽略(各模板文档的参数表均有此注明,见 strings/structured-swift5.md)。- 命名参数必须提供完整签名:函数参数可以使用任意名称甚至省略外部名,但只要使用了命名参数,就必须在配置值中写出包含全部命名参数的完整签名,例如
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!
相关推荐
PixiJS 资源清单(Manifest)与资源包(Bundle)加载实战指南
PixiJS 资源清单(Manifest)与资源包(Bundle)加载实战指南 本文将深入讲解 PixiJS 的 Manifest(资源清单) 与 Bundle
前端图形学SwiftGen与Combine框架:响应式资源加载
SwiftGen与Combine框架:响应式资源加载 在iOS开发中,资源加载往往依赖于字符串硬编码和手动管理,这不仅容易出错,还难以适应现代应用的响应式需求。
开发工具代码生成SwiftGen配置文件加密:保护敏感资源路径与参数
SwiftGen配置文件加密:保护敏感资源路径与参数 在iOS/macOS开发中,配置文件常包含API密钥、私有资源路径等敏感信息。SwiftGen默认以明文存
开发工具代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考