Das neue iOS Mobile Enterprise. Teil 1: Codegenerierung für Ressourcen

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.


Lokalisierungsfehler

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:


  • Die Anwendung stürzt ab, weil Sie den Namen des Bildes falsch angegeben und das Entpacken erzwungen haben
    UIImage(named: "NotExist")! 
  • Die Anwendung stürzt ab, wenn dem Ziel kein Storyboard hinzugefügt wird
  • Die Anwendung stürzt ab, wenn Sie einen Controller aus einem Storyboard mit einer nicht vorhandenen ID erstellt haben
  • Die Anwendung stürzt ab, wenn Sie einen Controller aus dem Storyboard mit einer vorhandenen ID erstellt, aber in die falsche Klasse umgewandelt haben
  • Unvorhersehbares Verhalten, wenn Sie eine Schriftart in Code verwenden, der nicht zu info.plist hinzugefügt wurde, oder wenn die Schriftartdatei nicht mit target gekennzeichnet ist: Absturz ist möglich, oder es ist möglich, einfach eine Standardschrift anstelle der von Ihnen benötigten zu erhalten. Entwickler Apple: Benutzerdefinierte Schriftarten , Stackoverflow: Absturz
  • Die Anwendung stürzt ab, wenn die Storyboards dem Controller eine Klasse anzeigen, die nicht vorhanden ist
  • Eine Reihe von eintönigem Code, der Symbole, Schriftarten, Controller und Ansichten erstellt
  • Es gibt keine Bilder, Symbole zur Laufzeit, obwohl sich der Name des Bildes im Storyboard befindet, jedoch nicht in Assets
  • Das Storyboard verwendet eine Schriftart, die nicht in info.plist enthalten ist
  • Zeilen-IDs werden in der Anwendung angezeigt, anstatt an unerwarteten Stellen lokalisiert zu werden, da Zeilen in Localizable.strings gelöscht werden (obwohl sie nicht verwendet wurden).
  • Etwas anderes, das ich vergessen habe zu erwähnen, oder dem wir noch nicht begegnet sind.

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.


Warnung

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!


Halt die Klappe und nimm mein Geld!

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
    Magie

     pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods' #           LocalPods,     ,   pod_target_srcroot = pod_target.pod_target_srcroot #   pod_target_path = pod_target_srcroot.sub('${PODS_ROOT}/..', '.') #       pod_target_sources_path = pod_target_path + '/' + pod_target.name + '/Sources' #     Sources generated_file_path = pod_target_sources_path + '/R.generated.swift' #     R.generated.swift File.new(generated_file_path, 'w') #    R.generated.swift      end end end 



  • 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| # ... generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD # ... end 

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| # ... s.dependency 'R.swift' r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end 

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| # map development pods installer.development_pod_targets.each do |target| # get only main spec and exclude subspecs spec = target.non_test_specs.first # get full podspec file path podspec_file_path = spec.defined_in_file # get podspec dir path pod_directory = podspec_file_path.parent # check if path contains local pods directory # exclude development but non local pods local_pods_directory_name = "LocalPods" if pod_directory.to_s.include? local_pods_directory_name # go to pod root directorty and run prepare command in sub-shell system("cd \"#{pod_directory}\"; #{spec.prepare_command}") end end end 

Und das gleiche Skript, das nicht für lokale Herde ausgeführt wurde


 Pod::Spec.new do |s| # ... s.dependency 'R.swift' generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end 

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

Source: https://habr.com/ru/post/de431148/


All Articles