大概,在每个大型的iOS项目中,这个寿命长的项目可能会偶然发现没有在任何地方使用过的图标,或者访问很长时间不存在的本地化密钥。 大多数情况下,这种情况是由注意力不集中引起的,自动化是解决注意力不集中的最佳方法。
在HeadHunter的iOS团队中,我们非常重视自动化开发人员可能遇到的例行任务。 在这篇文章中,我们希望开始一系列有关简化我们的日常工作的工具和方法的故事。
不久前,我们设法使用SwiftGen实用程序来控制应用程序资源。 关于如何配置它,如何使用它以及该实用程序如何帮助将对资源相关性的检查转移到编译器,我们将讨论cat。

SwiftGen是一个实用程序,可让您生成Swift代码以访问Xcode项目的各种资源,其中包括:
每个人都可以编写类似的代码来初始化图像或本地化字符串:
logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current )
为了表示图像或本地化键的名称,我们使用字符串文字。 双引号之间写的内容未经编译器或开发环境(Xcode)验证。 其中存在以下问题集:
- 您可以打错字;
- 您可能会在编辑或删除密钥/图像后忘记更新代码中的用法。
让我们看看如何使用SwiftGen改进此代码。
对于我们的团队而言,生成只与字符串和资产相关,它们将在本文中进行讨论。 其他类型资源的生成类似,并且,如果需要,可以轻松地独立掌握。
项目实施
首先,您需要安装SwiftGen。 我们选择通过CocoaPods安装它,以方便在所有团队成员之间分发实用程序。 但这可以通过其他方式完成, 文档中对此进行了详细介绍。 在我们的例子中,所有要做的就是将pod 'SwiftGen'
添加pod 'SwiftGen'
,然后添加一个新的构建阶段( Build Phase
),该Build Phase
将在构建项目之前启动SwiftGen。
"$PODS_ROOT"/SwiftGen/bin/swiftgen
在开始“ Compile Sources
阶段之前运行SwiftGen很重要,以避免在编译项目时出错。

现在我们准备将SwiftGen适应我们的项目。
配置SwiftGen
首先,您需要配置模板,通过这些模板将生成用于访问资源的代码。 该实用程序已经包含了一组用于生成代码的模板,它们都可以在github上查看,并且原则上可以使用。 模板是以Stencil语言编写的,如果您使用Sourcery或与Kitura一起玩 ,您可能会熟悉它。 如果需要,每个模板都可以适应其自己的指南。
例如,采用enum
生成的模板来访问本地化字符串。 在我们看来,标准中有太多多余的东西,可以简化。 扰流板下面是一个带有解释性注释的简化示例。
范本范例 {# #} {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} {# #} {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} _ p{{forloop.counter}}: {{type}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} p{{forloop.counter}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {# enum #} {% macro recursiveBlock table item sp %} {{sp}}{% for string in item.strings %} {{sp}}{% if not param.noComments %} {{sp}}/// {{string.translation}} {{sp}}{% endif %} {{sp}}{% if string.types %} {{sp}}{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { {{sp}} return localize("{{string.key}}", {% call argumentsBlock string.types %}) {{sp}}} {{sp}}{% else %} {{sp}}{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = localize("{{string.key}}") {{sp}}{% endif %} {{sp}}{% endfor %} {{sp}}{% for child in item.children %} {{sp}}{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {{sp}}{% set sp2 %}{{sp}} {% endset %} {{sp}}{% call recursiveBlock table child sp2 %} {{sp}}} {{sp}}{% endfor %} {% endmacro %} import Foundation {# enum #} {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} enum {{enumName}} { {% if tables.count > 1 %} {% for table in tables %} {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {% call recursiveBlock table.name table.levels " " %} } {% endfor %} {% else %} {% call recursiveBlock tables.first.name tables.first.levels " " %} {% endif %} } {# enum Localization #} extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } }
将模板文件本身保存在项目的根目录中非常方便,例如,保存在SwiftGen/Templates
文件夹中,以便该模板可供项目中的每个人使用。
该实用程序通过YAML文件swiftgen.yml
支持配置,您可以在其中指定源文件,模板和其他参数的路径。 在Swiftgen
文件夹的项目根目录中创建它,然后将与脚本相关的其他文件分组到同一文件夹中。
对于我们的项目,此文件可能如下所示:
xcassets: - paths: ../SwiftGenExample/Assets.xcassets templatePath: Templates/ImageAssets.stencil output: ../SwiftGenExample/Image.swift params: enumName: Image publicAccess: 1 noAllValues: 1 strings: - paths: ../SwiftGenExample/en.lproj/Localizable.strings templatePath: Templates/LocalizableStrings.stencil output: ../SwiftGenExample/Localization.swift params: enumName: Localization publicAccess: 1 noComments: 0
实际上,文件和模板的路径以及在其中传递到模板上下文的其他参数都在此处指示。
由于该文件不是项目的根目录,因此在启动Swiftgen时我们需要指定其路径。 更改我们的启动脚本:
"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml
现在我们的项目可以组装了。 组装后,两个文件Localization.swift
和Image.swift
应该沿着Image.swift
指定的路径出现在项目文件夹中。 它们需要添加到Xcode项目中。 在我们的情况下,生成的文件包含以下内容:
对于字符串: public enum Localization { public enum Languages { public enum ObjectiveC {
对于图像: public enum Image { public enum Logos { public static var objectiveC: UIImage { return image(named: "ObjectiveC") } public static var swift: UIImage { return image(named: "Swift") } } private static func image(named name: String) -> UIImage { let bundle = Bundle(for: BundleToken.self) guard let image = UIImage(named: name, in: bundle, compatibleWith: nil) else { fatalError("Unable to load image named \(name).") } return image } } private final class BundleToken {}
现在,您可以将生成的图像替换为UIImage(named: "")
形式的图像的本地化和初始化字符串UIImage(named: "")
所有用法。 这将使我们更容易跟踪对更改或删除本地化字符串键的更改。 在上述任何一种情况下,只有在修复了与更改相关的所有错误之后,项目才可以组装。
更改之后,我们的代码如下所示:
let logos = Image.Logos.self let localization = Localization.self private func setupWithLanguage(_ language: ProgrammingLanguage) { switch language { case .Swift: logoImageView.image = logos.swift nameLabel.text = localization.Languages.Swift.name descriptionLabel.text = localization.Languages.Swift.description wikiUrl = localization.Languages.Swift.link.toURL() case .ObjectiveC: logoImageView.image = logos.objectiveC nameLabel.text = localization.Languages.ObjectiveC.name descriptionLabel.text = localization.Languages.ObjectiveC.description wikiUrl = localization.Languages.ObjectiveC.link.toURL() } }
在Xcode中设置项目
生成的文件存在一个问题:它们可能会被错误地手动更改,并且由于每次编译都会从头开始覆盖它们,所以这些更改可能会丢失。 为避免这种情况,您可以在SwiftGen
脚本后锁定文件以进行写入。
这可以使用chmod
来实现。 我们通过启动SwiftGen重写了Build Phase
,如下所示:
PODSROOT="$1" OUTPUT_FILES=() COUNTER=0 while [ $COUNTER -lt ${SCRIPT_OUTPUT_FILE_COUNT} ]; do tmp="SCRIPT_OUTPUT_FILE_$COUNTER" OUTPUT_FILES+=(${!tmp}) COUNTER=$[$COUNTER+1] done for file in "${OUTPUT_FILES[@]}" do if [ -f $file ] then chmod a=rw "$file" fi done $PODSROOT/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml for file in "${OUTPUT_FILES[@]}" do chmod a=r "$file" done
该脚本非常简单。 在开始生成之前,如果文件存在,我们将授予它们写入权限。 运行脚本后,我们阻止了修改文件的功能。
为了便于编辑和检查脚本以进行检查,将其放置在单独的runswiftgen.sh
文件中非常方便。 可以在此处找到经过少量修改的脚本的最终版本。 现在,我们的Build Phase
将如下所示:我们将Pods文件夹的路径传递到脚本输入:
"$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT"
我们重建项目,现在当您尝试手动修改生成的文件时,将出现警告:

因此,带有Swiftgen的文件夹现在包含一个配置文件,一个用于锁定文件并启动Swiftgen
的脚本以及一个带有自定义模板的文件夹。 如有必要,可以方便地将其添加到项目中以进行进一步编辑。

而且由于Localization.swift
和Image.swift
自动生成的,因此您可以将它们添加到.gitignore,这样您就不必在git merge
再次git merge
后解决冲突。
如果您的项目使用Xcode New Build System(它是Xcode 10的默认构建系统),我还要提请注意需要为SwiftGen阶段构建指定输入/输出文件。 在“输入文件”列表中,必须指定生成代码所依据的所有文件。 在我们的例子中,这是脚本本身,配置,模板,.strings和.xcassets文件:
$(SRCROOT)/SwiftGen/runswiftgen.sh $(SRCROOT)/SwiftGen/swiftgen.yml $(SRCROOT)/SwiftGen/Templates/ImageAssets.stencil $(SRCROOT)/SwiftGen/Templates/LocalizableStrings.stencil $(SRCROOT)/SwiftGenExample/en.lproj/Localizable.strings $(SRCROOT)/SwiftGenExample/Assets.xcassets
在输出文件中,我们放置SwiftGen生成代码的文件:
$(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift
这些文件的指示是必需的,以便构建系统可以根据文件中是否存在更改以及在程序集的什么时候应执行此脚本来决定是否运行脚本。
总结
在处理项目资源时,SwiftGen是防止我们粗心大意的好工具。 有了它,我们能够自动生成代码以访问应用程序资源,并把检查资源与编译器的相关性的部分工作转移到编译器上,这意味着可以简化我们的工作。 另外,我们设置了Xcode项目,以便进一步方便使用该工具。
优点:
- 更容易控制项目资源。
- 减少错字错误,可以使用自动替换。
- 在编译阶段检查错误。
缺点:
- 不支持Localizable.stringsdict。
- 不考虑未使用的资源。
完整的例子可以在github上看到