Kontrolle über Ressourcen Anpassen von SwiftGen

  • Tutorial

In jedem großen iOS-Projekt kann es sein, dass man über Symbole stolpert, die nirgendwo verwendet werden, oder auf Lokalisierungsschlüssel zugreifen kann, die seit langem nicht mehr existieren. In den meisten Fällen entstehen solche Situationen aufgrund von Unaufmerksamkeit, und Automatisierung ist das beste Mittel gegen Unaufmerksamkeit.


Im HeadHunter iOS-Team legen wir großen Wert auf die Automatisierung von Routineaufgaben, mit denen Entwickler konfrontiert sind. Mit diesem Artikel möchten wir einen Zyklus von Geschichten über die Werkzeuge und Ansätze beginnen, die unsere tägliche Arbeit vereinfachen.


Vor einiger Zeit ist es uns gelungen, die Anwendungsressourcen mithilfe des Dienstprogramms SwiftGen zu übernehmen. Wie man es einrichtet, wie man damit umgehen kann und wie dieses Dienstprogramm dazu beiträgt, die Überprüfung der Relevanz von Ressourcen durch den Compiler zu verschieben, wird im Abschnitt "Cut" erläutert.



SwiftGen ist ein Dienstprogramm, mit dem Sie Swift-Code für den Zugriff auf verschiedene Xcode-Projektressourcen generieren können, darunter:


  • Schriftarten;
  • Farben;
  • Storyboards;
  • Lokalisierungszeichenfolgen;
  • Vermögenswerte.

Ein solcher Code zum Initialisieren von Bildern oder Lokalisierungszeichenfolgen kann von jedem geschrieben werden:


logoImageView.image = UIImage(named: "Swift")
nameLabel.text = String(
    format: NSLocalizedString("languages.swift.name", comment: ""),
    locale: Locale.current
)

Um den Namen eines Bildes oder eines Lokalisierungsschlüssels anzugeben, verwenden wir String-Literale. Was zwischen Anführungszeichen steht, wird vom Compiler oder der Entwicklungsumgebung (Xcode) nicht überprüft. Dies ist die folgende Gruppe von Problemen:


  • Sie können einen Tippfehler machen.
  • Sie können vergessen, die Verwendung des Codes nach dem Bearbeiten oder Löschen des Schlüssels / Bildes zu aktualisieren.

Mal sehen, wie wir diesen Code mit SwiftGen verbessern können.


Für unser Team war die Generierung nur für Linien und Assets relevant, und wir werden im Artikel darüber sprechen. Die Generierung für andere Ressourcentypen ist ähnlich und kann auf Wunsch problemlos unabhängig voneinander gemeistert werden.


Einführung in das Projekt


Zuerst müssen Sie SwiftGen installieren. Wir haben uns dazu entschieden, es über CocoaPods zu installieren, um das Dienstprogramm bequem auf alle Teammitglieder zu verteilen. Dies kann jedoch auf andere Weise erfolgen, die in der Dokumentation ausführlich beschrieben wird . In unserem Fall müssen Sie nur das Podfile pod 'SwiftGen'hinzufügen und dann eine neue Buildphase ( Build Phase) hinzufügen, die SwiftGen startet, bevor Sie das Buildprojekt starten.


"$PODS_ROOT"/SwiftGen/bin/swiftgen

Es ist wichtig, SwiftGen vor dem Start der Phase auszuführen Compile Sources, um Fehler beim Kompilieren des Projekts zu vermeiden.



Nun können Sie SwiftGen für unser Projekt anpassen.


SwiftGen-Setup


Der erste Schritt besteht darin, die Vorlagen zu konfigurieren, die den Code für den Zugriff auf Ressourcen generieren. Das Dienstprogramm enthält bereits eine Reihe von Vorlagen zum Generieren von Code, die alle auf der Githaba angezeigt werden können und grundsätzlich einsatzbereit sind. Vorlagen sind in der Stencil- Sprache geschrieben . Vielleicht kennen Sie sie, wenn Sie Sourcery oder Kitura verwendet haben . Auf Wunsch kann jede der Vorlagen an ihre Führungen angepasst werden.
Nehmen Sie beispielsweise eine Vorlage, die generiert wird, enumum auf Lokalisierungszeichenfolgen zuzugreifen. Es schien uns, dass im Standard zu viel überflüssig ist und vereinfacht werden kann. Ein vereinfachtes Beispiel mit erläuternden Kommentaren befindet sich unter dem Spoiler.


Mustervorlage
{# Обработка одного из входных параметров #}
{% 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
        )
    }
}

Die Vorlagendatei selbst wird bequem im Projektstamm gespeichert, z. B. in einem Ordner, SwiftGen/Templatesso dass diese Vorlage für alle verfügbar ist, die an dem Projekt arbeiten.
Das Dienstprogramm unterstützt die Anpassung über eine YAML-Datei swiftgen.yml, in der Sie den Pfad zu den Quelldateien, Vorlagen und erweiterten Parametern angeben können. Erstellen Sie es im Projektstammverzeichnis im Ordner Swiftgen, gruppieren Sie später die anderen mit dem Skript verknüpften Dateien in demselben Ordner.
Für unser Projekt kann 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 die Pfade zu den 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 den Pfad dazu angeben, wenn Sie Swiftgen starten. Ändern wir unser Startskript:


"$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml

Jetzt kann unser Projekt zusammengestellt werden. Nach dem Zusammenstellen im Projektordner swiftgen.ymlsollten zwei Dateien in den Pfaden Localization.swiftund angezeigt werden 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 {
            /// 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
        )
    }
}

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 ist es möglich, alle Verwendungen der Lokalisierungs- und Initialisierungszeilen der Ansichtsbilder UIImage(named: "")durch das zu ersetzen, was wir erzeugt haben. Dies erleichtert es uns, Änderungen in Schlüsseln von Lokalisierungszeichenfolgen zu verfolgen oder sie zu löschen. In jedem dieser Fälle erfüllt sich das Projekt einfach nicht, bis alle mit den Änderungen verbundenen Fehler behoben wurden.
Nach den Änderungen sieht unser Code so 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()
        }
    }

Ein Projekt in Xcode einrichten


Es gibt ein Problem mit den generierten Dateien: Sie können aus Versehen manuell geändert werden. Da sie bei jeder Kompilierung von Grund auf überschrieben werden, können diese Änderungen verloren gehen. Um dies zu vermeiden, können Sie nach Ausführung des Skripts Dateien im Datensatz blockieren SwiftGen.
Dies kann mit dem Befehl erreicht werden chmod. Wir schreiben unsere Build Phasemit der Einführung von SwiftGen wie folgt um:


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

Das Skript ist ziemlich einfach. Wenn die Dateien vorhanden sind, erteilen wir ihnen vor dem Starten der Generierung Schreibberechtigungen. Nach der Ausführung des Skripts blockieren wir die Möglichkeit, Dateien zu ändern.
Um das Skript bearbeiten und überprüfen zu können, sollten Sie es in einer separaten Datei ablegen runswiftgen.sh. Die endgültige Version des Skripts mit einigen Änderungen finden Sie hier. Jetzt sehen wir Build Phaseso aus: Beim Eingang des Skripts übergeben wir den Pfad zum Stammordner des Projekts und den Pfad zum Pods-Ordner:


"$SRCROOT"/SwiftGen/runswiftgen.sh "$SRCROOT" "$PODS_ROOT"

Wir erstellen das Projekt neu. Wenn Sie nun versuchen, die generierte Datei manuell zu ändern, wird eine Warnung angezeigt:



So enthält der Swiftgen-Ordner jetzt eine Konfigurationsdatei, ein Skript zum Blockieren und Starten von Dateien Swiftgensowie einen Ordner mit benutzerdefinierten Vorlagen. Es ist zweckmäßig, es dem Projekt zur weiteren Bearbeitung bei Bedarf hinzuzufügen.



Und als Dateien Localization.swiftund Image.swiftautomatisch generiert werden, können Sie sie in .gitignore, fügen Sie einmal wieder nicht Konflikte in ihrer nach auflösen git merge.


Ergebnisse


SwiftGen ist ein gutes Werkzeug zum Schutz vor Nachlässigkeit bei der Arbeit mit Projektressourcen. Damit ist es uns gelungen, automatisch Code für den Zugriff auf Anwendungsressourcen zu generieren und die Überprüfung der Relevanz von Ressourcen auf den Schultern des Compilers zu verschieben. Dies bedeutet, dass wir unsere Arbeit ein wenig vereinfachen können. Darüber hinaus haben wir das Xcode-Projekt konfiguriert, um die weitere Arbeit mit dem Tool komfortabler zu gestalten.


Pros:


  1. Einfachere Kontrolle der Projektressourcen.
  2. Die Wahrscheinlichkeit von Tippfehlern sinkt, die Verwendung der automatischen Ersetzung wird möglich.
  3. Fehler werden zur Kompilierzeit geprüft.

Nachteile:


  1. Keine Unterstützung für Localizable.stringsdict.
  2. Ressourcen, die nicht verwendet werden, werden nicht gezählt.

Ein vollständiges Beispiel ist auf der Githaba zu sehen .


Jetzt auch beliebt: