Wahrscheinlich kann der Langlebige in jedem großen iOS-Projekt auf Symbole stoßen, die nirgendwo verwendet werden, oder auf Lokalisierungsschlüssel, die schon lange nicht mehr existieren. Meistens entstehen solche Situationen durch Unaufmerksamkeit, und Automatisierung ist das beste Mittel gegen Unaufmerksamkeit.
Im iOS-Team von HeadHunter legen wir großen Wert auf die Automatisierung der Routineaufgaben, denen Entwickler möglicherweise begegnen. Mit diesem Artikel möchten wir einen Zyklus von Geschichten über jene Werkzeuge und Ansätze beginnen, die unsere tägliche Arbeit vereinfachen.
Vor einiger Zeit ist es uns gelungen, die Kontrolle über Anwendungsressourcen mithilfe des Dienstprogramms SwiftGen zu übernehmen. Wie man es konfiguriert, wie man damit lebt und wie dieses Dienstprogramm dabei hilft, die Überprüfung der Relevanz von Ressourcen auf die Schultern des Compilers zu verlagern, und wir werden über cat sprechen.

SwiftGen ist ein Dienstprogramm, mit dem Sie Swift-Code für den Zugriff auf verschiedene Ressourcen eines Xcode-Projekts generieren können, darunter:
- Schriftarten
- Farben
- Storyboards;
- Lokalisierungszeichenfolgen;
- Vermögenswerte.
Jeder kann einen ähnlichen Code schreiben, um Bilder oder Lokalisierungszeichenfolgen zu initialisieren:
logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current )
Um den Namen des Bildes oder des Lokalisierungsschlüssels anzugeben, verwenden wir Zeichenfolgenliterale. Was zwischen doppelten Anführungszeichen geschrieben wird, wird vom Compiler oder der Entwicklungsumgebung (Xcode) nicht überprüft. Darin liegen die folgenden Probleme:
- Sie können einen Tippfehler machen;
- Möglicherweise vergessen Sie, die Verwendung im Code zu aktualisieren, nachdem Sie den Schlüssel / das Bild bearbeitet oder gelöscht haben.
Mal sehen, wie wir diesen Code mit SwiftGen verbessern können.
Für unser Team war die Generierung nur für Zeichenfolgen und Assets relevant und wird im Artikel behandelt. Die Generierung für andere Arten von Ressourcen ist ähnlich und kann, falls gewünscht, problemlos unabhängig gemeistert werden.
Projektdurchführung
Zuerst müssen Sie SwiftGen installieren. Wir haben uns für die Installation über CocoaPods entschieden, um das Dienstprogramm bequem auf alle Teammitglieder zu verteilen. Dies kann jedoch auch auf andere Weise erfolgen, die in der Dokumentation ausführlich beschrieben werden. In unserem Fall müssen Sie pod 'SwiftGen'
den pod 'SwiftGen'
hinzufügen und dann eine neue Build-Phase ( Build Phase
) hinzufügen, die SwiftGen vor dem Erstellen des Projekts startet.
"$PODS_ROOT"/SwiftGen/bin/swiftgen
Es ist wichtig, SwiftGen vor dem Start der Phase " Compile Sources
kompilieren" auszuführen, um Fehler beim Kompilieren des Projekts zu vermeiden.

Jetzt können wir SwiftGen an unser Projekt anpassen.
Konfigurieren Sie SwiftGen
Zunächst müssen Sie die Vorlagen konfigurieren, mit denen der Code für den Zugriff auf die Ressourcen generiert wird. Das Dienstprogramm enthält bereits eine Reihe von Vorlagen zum Generieren von Code. Sie können alle auf dem Github angezeigt werden und sind im Prinzip einsatzbereit. Vorlagen sind in der Schablonensprache geschrieben. Sie sind möglicherweise damit vertraut, wenn Sie Sourcery verwendet oder mit Kitura gespielt haben . Auf Wunsch kann jede der Vorlagen an ihre eigenen Handbücher angepasst werden.
Nehmen Sie beispielsweise die Vorlage, die enum
generiert, um auf Lokalisierungszeichenfolgen zuzugreifen. Es schien uns, dass im Standard zu viel überflüssig ist und es vereinfacht werden kann. Ein vereinfachtes Beispiel mit erklärenden Kommentaren befindet sich unter dem Spoiler.
Vorlagenbeispiel {# #} {% 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 ) } }
Es ist praktisch, die Vorlagendatei selbst im Stammverzeichnis des Projekts zu speichern, z. B. im Ordner SwiftGen/Templates
damit diese Vorlage allen Personen zur Verfügung steht, die am Projekt arbeiten.
Das Dienstprogramm unterstützt die Konfiguration über die YAML-Datei swiftgen.yml
, in der Sie die Pfade zu den Quelldateien, Vorlagen und zusätzlichen Parametern angeben können. Erstellen Sie es im Stammverzeichnis des Projekts im Swiftgen
Ordner und gruppieren Sie später andere Dateien, die sich auf das Skript beziehen, in demselben Ordner.
Für unser Projekt könnte diese Datei folgendermaßen aussehen:
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
Tatsächlich werden dort die Pfade zu Dateien und Vorlagen sowie zusätzliche Parameter angegeben, die an den Vorlagenkontext übergeben werden.
Da sich die Datei nicht im Stammverzeichnis des Projekts befindet, müssen Sie beim Starten von Swiftgen den Pfad dazu angeben. Ändern Sie unser Startskript:
"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml
Jetzt kann unser Projekt zusammengestellt werden. Nach dem Zusammenbau sollten zwei Dateien Localization.swift
und Image.swift
im Projektordner entlang der in Image.swift
angegebenen Pfade Image.swift
. Sie müssen dem Xcode-Projekt hinzugefügt werden. In unserem Fall enthalten die generierten Dateien Folgendes:
Für Saiten: public enum Localization { public enum Languages { public enum ObjectiveC {
Für Bilder: 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 {}
Jetzt können Sie alle Verwendungen von Lokalisierungs- und Initialisierungszeichenfolgen von Bildern der Form UIImage(named: "")
durch das ersetzen, was wir generiert haben. Dies erleichtert es uns, Änderungen an Lokalisierungszeichenfolgenschlüsseln zu verfolgen oder diese zu entfernen. In jedem dieser Fälle wird das Projekt einfach erst zusammengestellt, wenn alle mit den Änderungen verbundenen Fehler behoben wurden.
Nach den Änderungen sieht unser Code folgendermaßen aus:
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() } }
Einrichten eines Projekts in Xcode
Es gibt ein Problem mit den generierten Dateien: Sie können versehentlich manuell geändert werden, und da sie bei jeder Kompilierung von Grund auf neu überschrieben werden, können diese Änderungen verloren gehen. Um dies zu vermeiden, können Sie Dateien zum Schreiben sperren, nachdem Sie SwiftGen
Skript ausgeführt haben.
Dies kann mit dem chmod
erreicht werden. Wir schreiben unsere Build Phase
mit dem Start von SwiftGen wie folgt um:
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
Das Skript ist ziemlich einfach. Wenn die Dateien vorhanden sind, erteilen wir ihnen vor Beginn der Generierung Schreibberechtigungen. Nach dem Ausführen des Skripts blockieren wir die Möglichkeit, Dateien zu ändern.
Um das Bearbeiten zu vereinfachen und das Skript auf Überprüfung zu überprüfen, ist es praktisch, es in einer separaten Datei runswiftgen.sh
. Die endgültige Version des Skripts mit geringfügigen Änderungen finden Sie hier . Jetzt sieht unsere Build Phase
aus: Wir übergeben den Pfad zum Pods-Ordner an die Skripteingabe:
"$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT"
Wir erstellen das Projekt neu. Wenn Sie nun versuchen, die generierte Datei manuell zu ändern, wird eine Warnung angezeigt:

Der Ordner mit Swiftgen enthält nun eine Konfigurationsdatei, ein Skript zum Sperren von Dateien und zum Starten von Swiftgen
sowie einen Ordner mit benutzerdefinierten Vorlagen. Es ist praktisch, es dem Projekt zur weiteren Bearbeitung hinzuzufügen, falls erforderlich.

Und da die Image.swift
Localization.swift
und Image.swift
automatisch generiert werden, können Sie sie zu .gitignore hinzufügen, damit Sie Konflikte nach dem erneuten git merge
nicht lösen müssen.
Ich möchte auch auf die Notwendigkeit aufmerksam machen, Eingabe- / Ausgabedateien für den SwiftGen-Phasenaufbau anzugeben, wenn Ihr Projekt das Xcode New Build System verwendet (es ist das Standard-Build-System mit Xcode 10). In der Liste Eingabedateien müssen Sie alle Dateien angeben, auf deren Grundlage der Code generiert wird. In unserem Fall ist dies das Skript selbst, die Konfiguration, Vorlagen, .strings- und .xcassets-Dateien:
$(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
In die Ausgabedateien legen wir die Dateien ab, in denen SwiftGen den Code generiert:
$(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift
Die Angabe dieser Dateien ist erforderlich, damit das Build-System entscheiden kann, ob das Skript ausgeführt werden soll, je nachdem, ob Änderungen in den Dateien vorhanden sind oder nicht, und zu welchem Zeitpunkt in der Assembly des Projekts dieses Skript ausgeführt werden soll.
Zusammenfassung
SwiftGen ist ein gutes Werkzeug zum Schutz vor Unachtsamkeit bei der Arbeit mit Projektressourcen. Damit konnten wir automatisch Code generieren, um auf Anwendungsressourcen zuzugreifen und einen Teil der Arbeit auf die Überprüfung der Relevanz von Ressourcen für den Compiler zu verlagern, was bedeutet, unsere Arbeit ein wenig zu vereinfachen. Darüber hinaus haben wir das Xcode-Projekt so eingerichtet, dass die weitere Arbeit mit dem Tool bequemer ist.
Vorteile:
- Einfachere Steuerung der Projektressourcen.
- Tippfehler werden reduziert, es wird möglich, die automatische Substitution zu verwenden.
- Fehler werden bei der Kompilierung überprüft.
Nachteile:
- Keine Unterstützung für Localizable.stringsdict.
- Nicht verwendete Ressourcen werden nicht berücksichtigt.
Das vollständige Beispiel finden Sie auf Github