Hallo allerseits!
Ich heiße Dmitry. So kam es, dass ich in den letzten zwei Jahren Teamleiter in einem Team von 13 iOS-Entwicklern bin. Und gemeinsam arbeiten wir an der Tinkoff Business- Anwendung.
Ich möchte Ihnen unsere Erfahrungen darüber mitteilen, wie Sie die Anwendung zu einem unerwarteten Zeitpunkt mit maximaler Anzahl von Funktionen oder Fehlerkorrekturen veröffentlichen können und trotzdem nicht grau werden.
Ich erzähle Ihnen von den Praktiken und Ansätzen, die dem Team geholfen haben, die Entwicklung und das Testen erheblich zu beschleunigen und die Menge an Stress, Fehlern und Problemen mit einer außerplanmäßigen oder dringenden Veröffentlichung erheblich zu reduzieren. #MakeReleaseWithoutStress .
Lass uns gehen!
Problembeschreibung
Stellen Sie sich die folgende Situation vor.
Es gibt eine andere Version. Dem gingen Regressionstests voraus. Die Tester fanden erneut eine Stelle, an der anstelle von Text in der Anwendung die Zeilen-ID angezeigt wird.

Dies war eines unserer häufigsten Probleme.
Dieses Problem tritt möglicherweise nicht auf, wenn die Anwendung nicht in einer anderen Sprache lokalisiert ist oder die gesamte Lokalisierung in Zeilen direkt im Code geschrieben ist, ohne die Datei Localizable.strings zu verwenden.
Möglicherweise stoßen Sie jedoch auf andere Probleme, die wir Ihnen bei der Lösung helfen:
Grund → Wirkung
Warum passiert das alles?
Es gibt Programmcode, der kompiliert wird. Wenn Sie etwas falsch geschrieben haben (syntaktisch oder wenn der Funktionsname beim Aufruf falsch ist), wird Ihr Projekt einfach nicht zusammengestellt. Das ist verständlich, offensichtlich und logisch.
Aber was ist mit Dingen wie Ressourcen?
Sie werden nicht kompiliert, sondern nach dem Kompilieren des Codes einfach zum Bundle hinzugefügt. In dieser Hinsicht kann zur Laufzeit eine große Anzahl von Problemen auftreten, beispielsweise der oben beschriebene Fall - mit Zeichenfolgen in der Lokalisierung.
Suche nach einer Lösung
Wir haben darüber nachgedacht, wie solche Probleme im Allgemeinen gelöst werden und wie wir dies beheben können. Ich erinnerte mich an eine der Cocoaheads-Konferenzen auf mail.ru. Es wurde über den Vergleich von Tools zur Codegenerierung gesprochen.
Nachdem wir noch einmal gesehen hatten, worum es bei diesen Tools (Bibliotheken / Frameworks) geht, fanden wir endlich heraus, was benötigt wurde.
Gleichzeitig verwenden Entwickler für Android seit Jahren einen ähnlichen Ansatz. Google hat über sie nachgedacht und sie zu einem sofort einsatzbereiten Tool gemacht. Aber Apple, selbst stabiler Xcode, kann uns nicht ...
Es blieb nur herauszufinden, welches Instrument zu wählen war: Natalie , SwiftGen oder R.swift ?
Natalie hatte keine Lokalisierungsunterstützung, es wurde beschlossen, sie sofort aufzugeben. SwiftGen und R.swift hatten sehr ähnliche Fähigkeiten. Wir haben uns für R.swift entschieden, einfach basierend auf der Anzahl der Sterne, da wir wussten, dass wir jederzeit zu SwiftGen wechseln können.
Wie R.swift funktioniert
Ein Skript für die Erstellungsphase vor der Kompilierung wird gestartet, durchläuft die Projektstruktur und generiert eine Datei mit dem Namen R.generated.swift
, die dem Projekt hinzugefügt werden muss (wir werden Ihnen am Ende mehr darüber erzählen).
Die Datei hat folgende Struktur:
import Foundation import Rswift import UIKit /// This `R` struct is generated and contains references to static resources. struct R: Rswift.Validatable { fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current fileprivate static let hostingBundle = Bundle(for: R.Class.self) static func validate() throws { try intern.validate() } // ... /// This `R.string` struct is generated, and contains static references to 2 localization tables. struct string { /// This `R.string.localizable` struct is generated, and contains static references to 1196 localization keys. struct localizable { /// en translation: Apple Pay /// /// Locales: en, ru static let card_actions_activate_apple_pay = Rswift.StringResource(key: "card_actions_activate_apple_pay", tableName: "Localizable", bundle: R.hostingBundle, locales: ["en", "ru"], comment: nil) // ... /// en translation: Apple Pay /// /// Locales: en, ru static func card_actions_activate_apple_pay(_: Void = ()) -> String { return NSLocalizedString("card_actions_activate_apple_pay", bundle: R.hostingBundle, comment: "") } } } }
Verwendung:
let str = R.string.localizable.card_actions_activate_apple_pay() print(str) > Apple Pay
"Warum brauchen Rswift.StringResource
?", Fragen Sie. Ich selbst verstehe nicht, warum ich es generieren soll, aber wie die Autoren erklären, wird es für Folgendes benötigt: Link .
Reale Anwendung
Eine kleine Erklärung des Inhalts unten:
* Es war - sie benutzten den Ansatz für eine Weile, am Ende verließen sie ihn
* Es ist - der Ansatz, den wir beim Schreiben von neuem Code verwenden
* War es nicht, aber Sie können es haben - ein Ansatz, den es in unserer Anwendung nie gab, aber ich habe ihn in verschiedenen Projekten kennengelernt, als ich noch nicht bei Tinkoff.ru gearbeitet habe.
Lokalisierung
Wir haben angefangen, R.swift
für die Lokalisierung zu verwenden. Das hat uns vor den Problemen R.swift
über die wir am Anfang geschrieben haben. Wenn sich die ID in der Lokalisierung geändert hat, wird das Projekt nicht zusammengestellt.
* Dies funktioniert nur, wenn Sie die ID in allen Lokalisierungen in eine andere ändern. Wenn eine Zeichenfolge in einer der Lokalisierungen verbleibt, wird beim Kompilieren eine Warnung angezeigt, dass diese ID nicht in allen Sprachen lokalisiert ist.

Nicht da, aber Sie könnten haben: final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } }
Es war: extension String { public func localized(in bundle: Bundle = .main, value: String = "", comment: String = "") -> String { return NSLocalizedString(self, tableName: nil, bundle: bundle, value: value, comment: comment) } } final class NewsViewController: UIViewController { private enum Localized { static let newsTitle = "news_title".localized() } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = Localized.newsTitle } }
Es wurde: titleLabel.text = R.string.localizable.newsTitle()
Bilder
Wenn wir nun etwas in * .xcassets umbenannt haben und den Code nicht geändert haben, wird das Projekt einfach nicht zusammengestellt.
Es war: imageView.image = UIImage(named: "NotExist") // imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash
Es wurde: imageView.image = R.image.tinkoffLogo() //
Storyboards
Es war: let someStoryboardName = "SomeStoryboard" // Change to something else (eg: "somestoryboard") - get nil or crash in else let someVCIdentifier = "SomeViewController" // Change to something else (eg: "someviewcontroller") - get nil or crash in else let storyboard = UIStoryboard(name: someStoryboardName, bundle: .main) let _vc = storyboard.instantiateViewController(withIdentifier: someVCIdentifier) guard let vc = _vc as? SomeViewController else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯}
Es wurde: guard let vc = R.storyboard.someStoryboard.someViewController() else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯ }
Usw.
Validierungs-Storyboard
R.validate () ist ein wunderbares Tool, das Hände schlägt (oder vielmehr nur einen Fehler in einen Catch-Block wirft), wenn Sie im Storyboard oder in den XIB-Dateien etwas falsch gemacht haben.
Zum Beispiel:
- Gibt den Namen des Bildes an, das nicht im Projekt enthalten ist
- Sie gaben die Schriftart an, verwendeten sie dann nicht mehr und löschten sie aus dem Projekt (aus info.plist).
Verwendung:
final class AppDelegate: UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { #if DEBUG do { try R.validate() } catch { // fatalError // debug // - , production fatalError(error.localizedDescription) } #endif return true } }
Und jetzt können Sie zwei kaufen!

Wie zu implementieren?
* Komponentenbasiertes System - ein Wiki , ein Codeentwicklungskonzept , bei dem Komponenten (eine Reihe von miteinander verbundenen Bildschirmen / Modulen) in einer geschlossenen Umgebung (in unserem Fall in lokalen Pods) entwickelt werden, um die Kohärenz der Codebasis zu verringern. Viele Leute kennen den Ansatz im Backend, der auf diesem Konzept basiert - Microservices.
* Monolith - Wiki , das Konzept der Codeentwicklung, bei dem die gesamte Codebasis in einem Repository liegt und der Code eng miteinander verbunden ist. Dieses Konzept eignet sich für kleine Projekte mit einem begrenzten Funktionsumfang.
Wenn Sie eine monolithische Anwendung entwickeln oder nur Abhängigkeiten von Drittanbietern verwenden, haben Sie Glück (dies ist jedoch nicht korrekt). Nehmen Sie das Tutorial und machen Sie alles streng darauf.
Dies war nicht unser Fall. Wir haben uns engagiert. Da wir ein komponentenbasiertes System verwenden und R.swift nicht nur in die Hauptanwendung einbetten, haben wir uns entschlossen, es in lokale Pods (die Komponenten sind) einzubetten.
Aufgrund der ständigen Aktualisierung von Lokalisierungen, Bildern und allen Elementen, die sich auf die Datei R.generated.swift auswirken, treten beim Zusammenführen mit dem gemeinsamen Zweig viele Konflikte in der generierten Datei auf. Und um dies zu vermeiden, sollten Sie R.generated.swift aus dem Git-Repository entfernen. Der Autor empfiehlt dies ebenfalls .
Fügen Sie die folgenden Zeilen zu .gitignore
.
# R.Swift generated files *.generated.swift
Wenn Sie für einige Ressourcen keinen Code generieren möchten, können Sie immer einzelne Dateien oder ganze Ordner ignorieren:
"${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore"
Beschreibung von .rswiftignore
Wie im Hauptprojekt war es für uns wichtig, keine R.generated.swift-Dateien aus lokalen Pods zum Git-Repository hinzuzufügen. Wir begannen darüber nachzudenken, wie dies getan werden könnte:
- Alias für R.generated.swift, sodass die Datei (Alias, zum Beispiel: R.swift) zum Projekt hinzugefügt wird und beim Kompilieren anhand der Referenz die reale Datei verfügbar ist. Aber Cocoapods sind schlau und dürfen das nicht
- Fügen Sie in podspec in der Vorkompilierungsphase die Datei R.generated.swift mithilfe von Skripten zum Projekt selbst hinzu. Anschließend wird sie einfach als Datei im Dateisystem hinzugefügt, und die Datei wird nicht im Projekt angezeigt
- andere mehr oder weniger nette Optionen
Magie in Podfile
Magie
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods'
- und eine andere Option ... füge immer noch R.generated.swift zu git hinzu
Wir haben uns vorübergehend für die Option "Magie im Podfile" entschieden, obwohl sie eine Reihe von Mängeln aufwies:
- Es konnte nur vom Projektstamm aus gestartet werden (obwohl Cocoapods von fast jedem Ordner im Projekt aus gestartet werden können).
- Alle Pods sollten einen Ordner namens Sources haben (obwohl dies nicht kritisch ist, wenn die Pods in Ordnung sind).
- Er war seltsam und unverständlich, aber früher oder später musste er unterstützen (dies ist immer noch eine Krücke)
- Befindet sich eine Bibliothek eines Drittanbieters in einem Ordner mit "LocalPods" im Pfad, wird versucht, die Datei "R.generated.swift" dort hinzuzufügen, oder es stürzt mit einem Fehler ab
prepare_command
Nachdem ich einige Zeit mit einem Drehbuch und Leiden gelebt hatte, beschloss ich, dieses Thema weiter zu studieren und fand eine andere Option.
In Podspec gibt es prepare_command , der nur zum Erstellen und Ändern der Quellen vorgesehen ist, die dann dem Projekt hinzugefügt werden.
* News - Der Name des Pods, der durch den Namen Ihres lokalen Pods ersetzt werden muss
* touch - Befehl zum Erstellen einer Datei. Das Argument ist der relative Pfad zur Datei (einschließlich des Namens der Datei mit der Erweiterung).
Als nächstes werden wir mit News.podspec Betrug begehen
Dieses Skript wird beim ersten Ausführen der pod install
aufgerufen und fügt die benötigte Datei dem Quellordner im Herd hinzu.
Pod::Spec.new do |s|
Als nächstes kommt eine weitere "Finte mit Ohren" - wir müssen das Skript R.swift für lokale Herde anrufen.
Pod::Spec.new do |s|
Es stimmt, es gibt ein "aber". prepare_command
funktioniert nicht mit lokalen Pods oder besser gesagt, aber in einigen besonderen Fällen. Es gibt eine Diskussion zu diesem Thema auf Github .
Todesfall
* Fatality - Wiki , der letzte Hit in Mortal Kombat.
Nach ein wenig mehr Recherche fand ich eine andere Lösung - eine Mischung aus Ansätzen c prepare_command
und pre_install
.
Eine kleine Modifikation der Magie aus dem Podfile:
pre_install do |installer|
Und das gleiche Skript, das nicht für lokale Herde ausgeführt wurde
Pod::Spec.new do |s|
Am Ende funktioniert es wie erwartet.
Endlich!
PS:
Ich habe versucht, einen anderen benutzerdefinierten Befehl anstelle von prepare_command
, aber pod lib lint
(ein Befehl zum Überprüfen des Podspec-Inhalts und des Herdes selbst) schwört auf zusätzliche Variablen und wird nicht übergeben.
Nicht lokale Herde
In Remote-Pods (die sich jeweils in einem eigenen Repository befinden) benötigen Sie nicht all diese oben beschriebene Skriptmagie, da dort die Codebasis streng an die Abhängigkeitsversion gebunden ist.
Es reicht aus, einfach das R.swift-Skript Example (ein Projekt, das nach dem Befehl pod lib create <Name> generiert wurde) selbst einzubetten und R.generated.swift zum Bibliothekspaket hinzuzufügen (unten). Wenn das Projekt kein Beispiel hat, müssen Sie Skripte schreiben, die denen ähneln, die ich zitiert habe.
PS:
Es gibt eine kleine Klarstellung:
R.swift + Xcode 10 + neues Build-System + inkrementeller Build! = <3
Weitere Informationen zum Problem finden Sie auf der Hauptseite der Bibliothek oder hier
R.swift v4.0.0 funktioniert nicht mit Cocoapods 1.6.0 :(
Ich denke, bald werden alle Probleme behoben sein.
Fazit
Sie müssen den Qualitätsbalken immer so hoch wie möglich halten. Dies ist besonders wichtig für Anwendungen, die mit Finanzen arbeiten.
In diesem Fall müssen Sie die Tests nicht überladen und Fehler so früh wie möglich finden. In unserem Fall ist dies entweder zum Zeitpunkt der Kompilierung des Codes durch den Entwickler oder beim Testlauf für Pull-Anforderungen. Daher finden wir den Mangel an Lokalisierung nicht durch den aufmerksamen Blick der Tester oder durch automatisierte Tests, sondern durch den üblichen Prozess der Erstellung der Anwendung.
Sie müssen auch die Tatsache berücksichtigen, dass dies ein Drittanbieter-Tool ist, das an die Projektstruktur gebunden ist und deren Inhalt analysiert. Wenn sich die Struktur der Projektdatei ändert, muss das Tool geändert werden.
Wir sind dieses Risiko eingegangen und sind in diesem Fall immer bereit, dieses Tool in ein anderes zu ändern oder Ihr eigenes zu schreiben.
Und der Gewinn von R.swift ist eine große Anzahl von Arbeitsstunden, die das Team für viel wichtigere Dinge aufwenden kann: neue Funktionen, neue technische Lösungen, Qualitätsverbesserungen und so weiter. R.swift hat den Zeitaufwand für die Integration vollständig zurückgegeben, auch unter Berücksichtigung des möglichen zukünftigen Ersatzes durch eine andere ähnliche Lösung.
R.swift
Bonus
Sie können mit einem Beispiel herumspielen, um den Gewinn aus der Codegenerierung für Ressourcen sofort mit eigenen Augen zu sehen. Der Quellcode des Projekts "herumspielen": GitHub .
Vielen Dank, dass Sie den Artikel gelesen haben oder einfach nur zu diesem Ort geblättert haben. Ich freue mich auf jeden Fall.)
Das ist alles