Probably, in every large iOS project - a long-liver, you can stumble upon icons that are not used anywhere, or access to localization keys that have not existed for a long time. Most often, such situations arise because of inattention, and automation is the best cure for inattention.
In the HeadHunter iOS team, we pay great attention to automating routine tasks that a developer may face. With this article we want to start a cycle of stories about those tools and approaches that simplify our daily work.
Some time ago we managed to take control of application resources using the SwiftGen utility. How to set it up, how to live with it and how this utility helps to shift the compiler’s checks on the relevance of resources, and will be discussed under the cut.
SwiftGen is a utility that allows you to generate Swift code to access various Xcode project resources, among them:
Such code for initializing images or localization strings could be written by everyone:
logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current )
To indicate the name of an image or a localization key, we use string literals. What is written between double quotes is not validated by the compiler or development environment (Xcode). This is the following set of problems:
Let's see how we can improve this code with SwiftGen.
For our team, generation was only relevant for strings and assets, which will be discussed in the article. Generation for other types of resources is similar and, if desired, is easily mastered independently.
First you need to install SwiftGen. We chose to install it via CocoaPods as a convenient way to distribute the utility among all team members. But this can be done in other ways, which are described in detail in the documentation . In our case, all that needs to be done is to add to the Podfile pod 'SwiftGen'
, then add a new build phase ( Build Phase
), which will launch SwiftGen before starting the build project.
"$PODS_ROOT"/SwiftGen/bin/swiftgen
It is important to run SwiftGen before starting the Compile Sources
phase to avoid errors when compiling a project.
Now you can begin to adapt SwiftGen for our project.
First of all, you need to configure the templates by which the code for accessing resources will be generated. The utility already contains a set of templates for generating code, all of them can be viewed on the githaba and, in principle, they are ready for use. The templates are written in the Stencil language, perhaps you are familiar with it if you used Sourcery or played with Kitura . If desired, each of the templates can be adapted to your guides.
For example, let's take a template that generates enum
for accessing localization strings. It seemed to us that in the standard too much is superfluous and it can be simplified. A simplified example with explanatory comments is under the spoiler.
{# #} {% 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 ) } }
The template file itself is conveniently saved in the project root, for example, in the SwiftGen/Templates
folder, so that this template is available to everyone who works on the project.
The utility supports customization through the YAML file swiftgen.yml
, in which you can specify the path to the source files, templates and advanced parameters. Create it in the project root in the Swiftgen
folder, later in the same folder we will group other files related to the script.
For our project, this file might look like this:
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
In fact, there are indicated the paths to the files and templates, as well as additional parameters that are passed to the template context.
Since the file is not in the root of the project, we need to specify the path to it when you start Swiftgen. Let's change our startup script:
"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml
Now our project can be assembled. After assembling in the project folder along the paths specified in swiftgen.yml
, two files should appear: Localization.swift
and Image.swift
. They need to be added to the Xcode project. In our case, the generated files contain the following:
public enum Localization { public enum Languages { public enum ObjectiveC { /// General-purpose, object-oriented programming language that adds Smalltalk-style messaging to the C programming language public static let description = localize("languages.objective-c.description") /// https://en.wikipedia.org/wiki/Objective-C public static let link = localize("languages.objective-c.link") /// Objective-C public static let name = localize("languages.objective-c.name") } public enum Swift { /// General-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, and Linux public static let description = localize("languages.swift.description") /// https://en.wikipedia.org/wiki/Swift_(programming_language) public static let link = localize("languages.swift.link") /// Swift public static let name = localize("languages.swift.name") } } public enum MainScreen { /// Language public static let title = localize("main-screen.title") public enum Button { /// View in Wikipedia public static let title = localize("main-screen.button.title") } } } extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } }
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 {}
Now it is possible to replace all uses of the localization and initialization lines of images of the UIImage(named: "")
type UIImage(named: "")
with what we generated. This will make it easier for us to track changes in keys of localization strings or remove them. In any of these cases, the project simply does not meet until all errors related to the changes are corrected.
After the changes, our code looks like this:
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() } }
There is one problem with the generated files: they can be changed manually by mistake, and since they are overwritten from scratch at each compilation, these changes may be lost. To avoid this, you can block files for writing after executing the SwiftGen
script.
This can be achieved with the chmod
. Rewrite our Build Phase
with SwiftGen launch as follows:
if [ -f "$SRCROOT"/SwiftGenExample/Image.swift ]; then chmod +w "$SRCROOT"/SwiftGenExample/Image.swift fi if [ -f "$SRCROOT"/SwiftGenExample/Localization.swift ]; then chmod +w "$SRCROOT"/SwiftGenExample/Localization.swift fi "$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml chmod -w "$SRCROOT"/SwiftGenExample/Image.swift chmod -w "$SRCROOT"/SwiftGenExample/Localization.swift
The script is pretty simple. Before starting the generation, if the files exist, we grant write permissions for them. After executing the script, we block the ability to change files.
For ease of editing and checking the script for a review, it is convenient to put it into a separate file, runswiftgen.sh
. The final version of the script with minor modifications can be found here. Now our Build Phase
will look like this: the input to the script is the path to the root folder of the project and the path to the folder Pods:
"$SRCROOT"/SwiftGen/runswiftgen.sh "$SRCROOT" "$PODS_ROOT"
Rebuilding the project, and now when trying to manually modify the generated file, a warning will appear:
So, the Swiftgen folder now contains a configuration file, a script to block files and launch Swiftgen
and a folder with customized templates. It is convenient to add it to the project for further editing if necessary.
And since the files Localization.swift
and Image.swift
generated automatically, you can add them to .gitignore, so that once again they do not resolve conflicts in them after git merge
.
SwiftGen is a good tool to protect against our carelessness when working with project resources. With it, we managed to automatically generate code to access application resources and shift some of the work of checking the relevance of resources on the shoulders of the compiler, which means we can simplify our work a bit. In addition, we set up the Xcode project so that further work with the tool was more convenient.
Pros:
Minuses:
A full example can be seen on the githaba.
Source: https://habr.com/ru/post/423381/
All Articles