In diesem Artikel geht es darum gewisse Schwächen von Xcode mithilfe von Tools und Programmierlösungen auszugleichen. Hierzu wird für jedes Thema zunächst das Problem mit Xcode und dessen mögliche Folgen in der Praxis erläutert, woraufhin ein Lösungsvorschlag gegeben wird.

Fehlende Aktualisierung von Übersetzungen

Problem

Xcode bietet die Möglichkeit mithilfe des in der Foundation Library definierten Makros NSLocalizedString(key, comment) Texte programmatisch zu lokalisieren. Dies ermöglicht das Anbieten einer App in vielen verschiedenen Programmiersprachen, deren Übersetzungen in den Localizable.strings-Dateien zusammengefasst sind (für jede Sprache eine eigene). Dabei wird im Quellcode lediglich ein Key verwendet, welcher in den .strings-Dateien die jeweiligen Texte in den jeweiligen Sprachen zugewiesen werden.

Eine Localizable.strings für die deutsche Sprache sieht etwa so aus:

"HOME.NAV_BAR.TITLE" = "Start";
"HOME.SETTINGS_BUTTON.NORMAL_TITLE" = "Einstellungen";
"SETTINGS.NAV_BAR.TITLE" = "Einstellungen";

Die zugehörige englische Version dann etwa so:

"HOME.NAV_BAR.TITLE" = "Home";
"HOME.SETTINGS_BUTTON.NORMAL_TITLE" = "Settings";
"SETTINGS.NAV_BAR.TITLE" = "Settings";

Im Code wird das Ganze dann folgendermaßen verwendet:

titleLabel.text = NSLocalizedString("HOME.NAV_BAR.TITLE", "")

Führt man eine App aus, sucht sie sich auf dem Endgerät je nach Geräteeinstellung die passenden Texte automatisch heraus und ersetzt die Keys, die im Quellcode verwendet wurden. Leider muss man in Xcode aktuell jedoch jeden neuen Key, den man im Code mittels NSLocalizedString verwendet in jeder einzelnen Sprachfassung der .strings Dateien einzeln händisch hinzufügen. Dies kann je nach Projekt sehr viel Zeit in Anspruch nehmen und ist zudem fehleranfällig.

Außerdem lassen sich in Xcode ähnlich wie beim NSLocalizedString-Makro auch textbasierte Interface-Elemente über Keys lokalisieren, wobei hier jedoch eine XIB-Datei oder ein Storyboard in der Sprache “Base” zum Einsatz kommt, worin das Design für alle Sprachen festgelegt wird (siehe Base Internationalization). Auch hier bietet Xcode keine Option, neu hinzugefügte Textelemente im Nachhinein in den dazugehörigen .strings-Dateien zu ergänzen, sondern erneut ist nervige händische Arbeit gefragt.

Fügt man in einem Storyboard oder einer XIB etwa ein neues UILabel-Element hinzu und vergibt dort den Text “Nutzername”, so bleiben die zugehörigen Strings-Dateien zum Storyboard oder XIB leer und erhalten nicht automatisch das neue UILabel als einen neuen Key-Eintrag. Die einzige Ausnahme bildet hier der Moment, in dem man ein Storyboard oder XIB erstmals lokalisiert. Beim Lokalisierungsvorgang durchsucht Xcode die Interface-Elemente und tut, was es eigentlich regelmäßig tun sollte, nämlich die gefundenen Keys in allen Sprachdateien automatisch anzulegen – dies ist in einer Welt mit verändernden Anforderungen jedoch keine asureichende Lösung. Hier muss dringend eine ergänzend funktionierende Lösung her.

Lösungsvorschlag

Das Open Source Tool BartyCrouch wurde genau zur Lösung dieser beiden Probleme geboren. Die Installation findet via Homebrew mit den Befehlen brew tap flinesoft/bartycrouch und brew install flinesoft/bartycrouch/bartycrouch statt. Anschließend konfiguriert man BartyCrouch mittels eines Build-Scripts, welches im einfachen Fall folgendermaßen aussieht:

if which bartycrouch > /dev/null; then
    # Incrementally update all Storyboards/XIBs strings files
    bartycrouch interfaces -p "$PROJECT_DIR/Sources"

    # Add new keys to Localizable.strings files from NSLocalizedString in code
    bartycrouch code -p "$PROJECT_DIR/Sources" -l "$PROJECT_DIR/Sources" -a
else
    echo "warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch"
fi

Wie man in einem Projekt ein Build Script hinzufügt wird hier ausführlich erklärt. Ist das einmal geschehen wird BartyCrouch fortan automatisch alle Base-lokalisierten Storyboards und XIBs im Projektordner durchsuchen und aus den gefundenen Texten Einträge in den Strings-Dateien erstellen. Das Gleiche wird auch mit Objective-C und Swift-Dateien getan und entsprechend die Localizable.strings Dateien aktualisiert.

Da man beim Entwickeln von Natur aus immer wieder builden muss, braucht man sich also nach dem einmaligen Einrichten des Skripts pro Projekt um nichts mehr zu kümmern, da nun alle neuen Lokalisierungen bei jedem Build aktuell gehalten werden. Die Übersetzungen werden hierbei übrigens leer gelassen, diese müssen dann natürlich noch ausgefüllt werden.

Ausnahmen

BartyCrouch funktioniert grundsätzlich so, dass es sämtliche übersetzbaren Interface-Elemente in Storyboards und XIBs in die .strings Dateien aufnimmt. Manchmal legt man jedoch etwa ein UILabel-Element im Interface Builder an mit der Absicht dessen Wert erst später programmatisch zu setzen – eine Übersetzung wäre dafür also sinnlos. In solchen Fällen bietet BartyCrouch die Möglichkeit mithilfe der Markierung #bc-ignore! das Ignorieren des Interface Elements bei der automatischen Übersetzung zu erzwingen. Das so markierte Element wird dann nicht in die .strings Dateien eingefügt. Dies kann etwa so aussehen:

Beispielbild mit teilweise automatisch generierten
String-Keys Hier werden die Werte (rechts) für die Bezeichner (links) programmatisch gesetzt und können deshalb von der Übersetzung ausgeschlossen werden.

Dynamisch referenzierte Ressourcen

Problem

Wie weiter oben bereits erläutert bietet die Foundation Library das Makro NSLocalizedString(key, comment) an, um Texte in Xcode-Projekten zu übersetzen. Der key ist dabei vom Typ String, was den großen Nachteil hat, dass alle Automatismen von statischer Typisierung beim Builden von Code nicht mehr greifen. Das bedeutet konkret, dass Xcode nicht überprüfen kann, ob zu den Werten, die als Keys im Code genutzt werden auch tatsächlich Übersetzungen existieren. So kann ein Tippfehler, eine Änderung des Keys im Code oder fehlende Übersetzungen zu neuen Keys zu dem Problem führen, dass am Ende leere Texte in der App angezeigt werden und dies beim Entwickeln und Builden gar nicht auffällt.

Gleiches gilt auch für Bilder, die mit UIImage(named:) durch einen String initialisiert werden. Xcode durchsucht dann automatisch sowohl die .xcassets-Ordner des Projekts, als auch alle direkt hinzugefügten Bilder und gleicht deren Namen mit dem übergebenen String ab, um so das korrekte Bild zu finden. Interfaces lädt man auf ähnliche Weise mittels Strings von Storyboards oder XIBs, Farben werden meist mit RGB-Werten bei Notwendigkeit ad-hoc angelegt. Analog zu den dynamisch referenzierten Übersetzungen verhält es sich auch bei per Strings referenzierten Bildern oder Interfaces. Tippfehler oder Umbenennungen im Code oder den jeweiligen Ressourcen führen dazu, dass beim Ausführen der App Bilder fehlen, Interfaces nicht gefunden werden können oder die Farben an vielen unterschiedlichen Stellen gesucht und geändert werden müssen. Xcode zeigt in Fehlerfällen weder Warnungen noch Fehlermeldungen an, da es die tatsächlich existierenden Ressourcen schlichtweg nicht kennt, weil sie dynamisch mit den Strings oder den RGB-Werten geladen werden.

Übersetzungen, Bilder, Interfaces und Farben seien im Folgenden unter “Ressourcen” zusammen gefasst.

Lösungsvorschlag

Ressourcen sollten statisch über automatisch generierten Code gepflegt und geladen werden, sodass man einerseits gar nicht mehr mit fehlenden Ressourcen builden kann (Xcode zeigt Fehlermeldungen an, wenn referenzierter Code nicht vorhanden ist) und andererseits eine zentrale Stelle zum Verwalten und Ändern hat. Letzteres ist vor allem bei den Farben wichtig, die in den meisten Apps einheitlich sein sollten. Das Tool SwiftGen leistet hierbei hervorragende Dienste und durchsucht das Projekt automatisch nach Übersetzungen, Bildern, Interfaces und Farben.

Installieren lässt sich SwiftGen am einfachsten mit dem Befehl brew install swiftgen, vorausgesetzt Homebrew ist installiert. Die Konfiguration in einem Projekt geschieht analog zu BartyCrouch mithilfe eines Build Scriptes, in welchem man die verschiedenen Befehle und Pfade, die man nutzen möchte angibt. Im Folgenden werden die einzelnen Funktionen erläutert, am Ende folgt ein Beispiel-Build-Script.

Übersetzungen

Was Übersetzungen betrifft untersucht SwiftGen automatisch alle .strings-Dateien auf ihre Keys und generiert aus diesen eine Code-Struktur namens Strings.swift worin sämtliche Keys per Dot-Syntax erreichbar sind und als Ergebnis die jeweilige Übersetzung liefern. Dies ermöglicht es alle Aufrufe von NSLocalizedString("SETTINGS.NAVIGATION_BAR.TITLE", comment) durch Aufrufe wie L10n.NavigationBar.Title zu ersetzen. Dies löst zum Einen das Problem der statischen Überprüfung, da Xcode bereits automatisch Code auf Existenz prüft, wenn er kompiliert wird und bietet als Nebeneffekt auch den Vorteil, dass man für sämtliche existierende Keys nun auch die Autovervollständigung nutzen kann, was bei reinen Strings nicht möglich wäre.

Zusätzlich zur Verwendung von SwiftGen sei an dieser Stelle noch eine Übersetzungs-Key-Struktur dabei empfohlen, die die Autovervollständigungsfunktion maximal ausreizt, Übersetzungskommentare als Kontext-Informanten überflüssig macht und das Unterscheiden von Übersetzungs-Keys und tatsächlichen Übersetzungen vereinfacht. Die Struktur sollte folgende Regeln einhalten:

  • Die Keys werden KOMPLETT IN GROSSBUCHSTABEN GESCHRIEBEN
  • Die Keys werden hierarchisch.mit.Punkten.getrennt
  • Trennungen zwischen Wörtern in den Keys werden mit_Unterstrichen_umgesetzt

Nachfolgend sind ein paar Beispiel-Keys aufgelistet, die diese Vorgaben einhalten:

SETTINGS.TITLE
SETTINGS.FONT_SECTION.SIZE
SETTINGS.FONT_SECTION.COLOR
MODEL.ARTICLE.TITLE
MODEL.ARTICLE.RELEASE_DATE

Mit SwiftGen können sie dann folgendermaßen verwendet werden:

self.title = L10n.Settings.Title
articleCell.titleLabel.text = L10n.Model.Article.Title

Farben

Zur Verwendung der Farbenfunktionalität sollte eine Colors.txt-Datei im Hauptverzeichnis des Projekts erstellt werden, deren Inhalt etwa folgendermaßen aussehen kann:

Primary     : #40657d
Secondary   : #657d40
Accent      : #b7d3e3

Die Benutzung der konfigurierten Farben sieht dann etwa so aus:

self.view.backgroundColor = UIColor(named: .Primary)
self.view.tintColor = UIColor(named: .Accent)

Bilder & Interfaces

Zur Nutzung der Bilder- und Interfaces-Funktionen ist keine spezielle Konfiguration vonnöten. Die Nutzung ist auch einfach:

let image = UIImage(asset: .Banana)
let wizardViewCtrl = StoryboardScene.Wizard.initialViewController()

Build Script

Das kombinierte Build Script, um alle obigen Funktionen zu nutzen, sieht wie folgt aus (auf Wunsch sind einzelne Zeilen entfernbar):

if which swiftgen > /dev/null; then
    swiftgen strings "$PROJECT_DIR/Sources" -t dot-syntax --output "$PROJECT_DIR/Sources/Constants/Strings.swift"
    swiftgen storyboards "$PROJECT_DIR/Sources" --output "$PROJECT_DIR/Sources/Constants/Storyboards.swift"
    swiftgen images "$PROJECT_DIR/Sources" --output "$PROJECT_DIR/Sources/Constants/Images.swift"
    swiftgen colors "$PROJECT_DIR/Colors.txt" --output "$PROJECT_DIR/Sources/Constants/Colors.swift"
else
    echo "warning: SwiftGen not installed, download it from https://github.com/AliSoftware/SwiftGen"
fi

Hinweis: Man beachte, dass beim Einfügen des Scripts in das Build-Script-Feld in Xcode die Formatierung des Textes verloren geht. Diese lässt sich mit ein paar Einrückungen jedoch schnell wieder herstellen, um den Code im Script lesbarer zu halten.

Die Ergebnis-Dateien werden im Ordner Sources/Constants abgelegt, welcher vorher erstellt werden sollte. Fortan werden die Dateien bei jedem Build neu erzeugt, sofern es seit der letzten Generierung Änderungen an den jeweiligen Ressourcen gab.

Fehlende Code Conventions und TODO-Warnungen

Swift erlaubt es wie die meisten Programmiersprachen das gleiche Funktionsverhalten einer App auf viele verschiedene Weisen zu implementieren. Während man im Allgemeinen nicht sagen kann, dass es “die perfekte” Implementierung einer Funktion gibt, kristallisieren sich dennoch mit der Zeit für jede Programmiersprache und Plattform eine ganze Menge von zu vermeidenden und eher zu bevorzugenden Implementierungen heraus. Um möglichst hohe Code-Qualität zu erreichen lohnt es sich daher für jede längerfristig angelegte Arbeit, besonders wenn mehrere Entwickler beteiligt sind Code-Konventionen zu erarbeiten und sich an diesen zu orientieren.

Während das offizielle Buch der Swift-Entwickler The Swift Programming Language beim Erläutern der einzelnen verfügbaren Funktionen bereits viele sinnvolle Beispiele für guten Programmierstil vorgibt, ist es diesbezüglich doch eher zu zeitaufwändig und unvollständig (wenn auch trotzdem lesenswert). Als offizielle Apple-Quelle seien ergänzend die API Design Guidelines empfohlen. Als bessere Alternative gibt es die zusammengefassten und mit Beispielen versehenen Swift Code Conventions von GitHub und von raywenderlich.com, an die auch wir uns bei Jamit Labs halten.

Das Open Source Projekt SwiftLint hat es sich zur Aufgabe gemacht die GitHub Conventions automatisiert in Xcode zu überprüfen. Nach einer Installation des SwiftLint-Tools via Homebrew (brew install swiftlint) kann nun jedes Projekt durch drei einfache Schritte um eine automatisierten Warnings- und Error-Generator bei Nichteinhaltung von bestimmten Code-Konventionen erweitert werden:

  • Füge ein Build-Script mit diesem Inhalt für das App- oder Framework-Target hinzu:
if which swiftlint > /dev/null; then
    swiftlint
else
    echo "warning: SwiftLint not installed, download it from https://github.com/realm/SwiftLint"
fi

Hinweis: Man beachte, dass beim Einfügen des Scripts in das Build-Script-Feld in Xcode die Formatierung des Textes verloren geht. Diese lässt sich mit ein paar Einrückungen jedoch schnell wieder herstellen, um den Code im Script lesbarer zu halten.

  • Erstelle danach im Hauptverzeichnis des Projekts eine Datei namens .swiftlint.yml mit folgender Konfiguration (bei Bedarf änderbar):
# some rules are only opt-in
opt_in_rules:
- empty_count
- missing_docs

# paths to include during linting. `--path` is ignored if present.
included:
- Sources

# paths to ignore during linting. Takes precedence over `included`.
excluded:
- Carthage
- Sources/Constants

# configurable rules can be customized from this configuration file
line_length: 180

Als drittes sollte nun noch eine Einstellung von Xcode angepasst werden, da die Regel trailing_newline sonst beim Standardverhalten von Xcode Warnungen wegen falsch gesetzter Leerzeichen meldet, die man ganz einfach vermeiden kann: Gehe in die Einstellungen von Xcode (“Preferences”), wähle dort den Reiter “Text Editing” und stelle sicher, dass neben “Automatically trim trailing whitespace” auch “Including whitespace-only lines” angehakt ist. Dies stellt sicher, dass bei Umbrüchen und leer gelassenen Zeilen entstehende Leerzeichen automatisch gelöscht werden. Hier kann übrigens auch gleich die Empfehlungslinie für die Zeilenbreite in Xcode angepasst werden unter “Page guide at column” - wir empfehlen 180 als Kompromiss zwischen ständigen Zeilenumbrüchen und Lesbarkeit von Code.

Diese drei Schritte sorgen dafür, dass bei jedem Entwickler, der den Swift Linter installiert hat automatisch Warnungen oder in bestimmten Fällen sogar Errors in Xcode generiert und angezeigt werden, wenn die konfigurierten Konventionen nicht eingehalten werden. Falls jemand das Tool SwiftLint nicht installiert hat wird wenigstens eine Warnung angezeigt, dass der Swift Linter nicht installiert ist. Als netten Nebeneffekt zeigt SwiftLint zusätzlich auch Code-Kommentare, die ein TODO: oder FIXME enthalten als Warnungen an, damit man diese nicht so schnell vergessen oder übersehen kann.