Control over resources. Set up SwiftGen
Probably, in every big iOS-project - long-livers you can come across icons that are not used anywhere, or access localization keys that have not existed for a long time. Most often, such situations arise because of inattention, and the best cure for inattention is automation.
In the iOS-command HeadHunter we pay much attention to automation of routine tasks, which the developer can face. With this article, we want to start a series of stories about the tools and approaches that simplify our daily work.
Some time ago we managed to take the application resources under control with the help of the SwiftGen utility. About how to configure it, how to live with it and how this utility helps to shift the verification of the relevance of resources to the shoulders of the compiler, and will be discussed under the cut.
documentation . In our case, all you need to do is add
to Podfile. pod 'SwiftGen'
, then add a new build phase ( Build Phase
), which will run SwiftGen before starting the project build.
"$ PODS_ROOT" /SwiftGen /bin /swiftgen
It is important to run SwiftGen before running phase Compile Sources
to avoid errors when compiling the project.
Now you can start adapting SwiftGen to our project.
Setting up SwiftGen
The first step is to configure the templates, which will generate code for accessing resources. The utility already contains a set of templates for code generation, they can all be viewed at githabe and, in principle, they are ready for use. Templates are written in the language Stencil , you may be familiar with it if you used Sourcery or played with Kitura . If desired, each of the templates can be adapted to their guides.
For example, take a template that generates enum
to access the localization strings. It seemed to us that in the standard there is too much too much and it can be simplified. A simplified example with explanatory comments is under the spoiler.
{# Processing one of the input parameters #}
{% set accessModifier%} {% if param.publicAccess%} public {% else%} internal {% endif%} {% endset%}
{# Declaring auxiliary macros #}
{% 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%}
{# Declare a macro that creates either a nested enum or a static constant to access the value #}
{% 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
{# Declare the root 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%}
}
{# Extend enum Localization for convenient conversion of the key to the required localization string #}
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 folder SwiftGen /Templates
so that this template is available to everyone who works on the project.
The utility supports configuration via YAML-file swiftgen.yml
, in which you can specify the path to the source files, templates and additional parameters. Create it in the project root in the folder Swiftgen
, in the same folder later, we group the other files associated with 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 specified paths to files and templates, as well as additional parameters that are passed to the context of the template.
Since the file does not lie in the root of the project, we need to specify the path to it when running Swiftgen. Let's change our startup script:
"$ PODS_ROOT" /SwiftGen /bin /swiftgen config run --config SwiftGen /swiftgen.yml
Now our project can be collected. After assembly in the project folder as specified in swiftgen.yml
two files should appear in the paths. 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-. 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 you can replace all the use of the localization and initialization lines of images of the form UIImage (named: "")
on what we have generated. This will make it easier for us to track changes to or delete key locale rows. In either of these cases, the project simply will not meet until all the errors associated with 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 ()
}
}
Setting up the project in Xcode
There is one problem with the generated files: they can be changed manually by mistake, and since they are overwritten from scratch every compilation, these changes can be lost. To avoid this, you can lock the files to write after the execution of the script SwiftGen
.
This can be achieved with the command chmod
. We rewrite our Build Phase
with the 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 quite simple. Before starting the generation, if the files exist, we issue write permissions for them. After running the script, we block the ability to modify the files.
For convenience of editing and checking the script on the review it is convenient to put it in a separate file runswiftgen.sh
. The final version of the script with some minor modifications can be found here. Now our Build Phase
will look like this: enter the script to the path to the project's root folder and the path to the Pods folder:
"$ SRCROOT" /SwiftGen/runswiftgen.sh "$ SRCROOT" "$ PODS_ROOT"
We rebuild the project, and now when you try to change the generated file manually, a warning will appear:
So, the folder with Swiftgen now contains a configuration file, a script for blocking files and running 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 are Localization.swift
and Image.swift
are generated automatically, they can be added to .gitignore, so once again they do not solve conflicts after git merge
.
Results of
SwiftGen is a good tool for protecting against our inattention when working with project resources. Using it, we managed to automatically generate code to access the resources of the application and to shift some of the work on checking the relevance of resources on the shoulders of the compiler, which means that we can simplify our work a little. In addition, we configured the Xcode project so that further work with the tool was more convenient.
Pros:
It is easier to control the resources of the project.
The probability of typos is reduced, it becomes possible to use auto-substitution.
Errors are checked at compile time.
Cons:
There is no support for Localizable.stringsdict.
Resources that are not used are not considered.
You can see the whole example at githabe
It may be interesting
weber
Author16-09-2018, 21:11
Publication DateDevelopment of mobile applications / Swift
Category- Comments: 1
- Views: 517
We have years of experience in repairing fences and we can give you one that you will love! No matter what type of fence you have, we can fix it for you. Check Out: All fences cape coral