Probleme mit Koordinatormustern und was hat RouteComposer damit zu tun?

Ich setze die Artikelserie über die RouteComposer- Bibliothek fort, die wir verwenden, und heute möchte ich über das Koordinatormuster sprechen. Ich wurde aufgefordert, diesen Artikel durch eine Diskussion über einen der Artikel über das Muster zu schreiben. Der Koordinator hier auf Habr.


Das vor nicht allzu langer Zeit eingeführte Koordinatormuster erfreut sich bei iOS-Entwicklern immer größerer Beliebtheit, und im Allgemeinen ist klar, warum. Weil die von UIKit bereitgestellten Tools kein universelles Durcheinander sind.


Bild


Ich habe bereits die Frage nach der Fragmentierung der Art und Weise aufgeworfen, wie ich die Ansicht von Controllern auf dem Stapel zusammenstelle. Um Wiederholungen zu vermeiden, können Sie hier einfach darüber lesen.


Seien wir ehrlich. Irgendwann stellte Epole fest, dass sie durch das Einfügen der Controller in das Anwendungsentwicklungszentrum keine sinnvolle Möglichkeit zum Erstellen oder Übertragen von Daten zwischen ihnen bot. Nachdem sie die Lösung für dieses Problem den Entwicklern anvertraut hatte, wurde sie automatisch von Xcode und möglicherweise von den UISearchConnroller-Entwicklern vervollständigt. Irgendwann wurden uns Storyboards und Segues vorgestellt. Dann erkannte Epolus, dass sie Anwendungen, die nur aus zwei Bildschirmen bestanden, selbst schrieb, und schlug in der nächsten Iteration die Möglichkeit vor, Storyboards in mehrere Komponenten aufzuteilen, da Xcode abstürzte, als das Storyboard eine bestimmte Größe erreichte. Segues haben sich zusammen mit diesem Konzept in mehreren Iterationen geändert, die nicht sehr kompatibel miteinander sind. Ihre Unterstützung ist eng in die massive UIViewController Klasse UIViewController , und am Ende haben wir das bekommen, was wir haben. Das hier:


 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } } 

Die Anzahl der Force-Tycastcasts in diesem Codeblock ist erstaunlich, ebenso wie die Zeichenfolgenkonstanten in den Storyboards selbst, um zu verfolgen, welche Xcodes überhaupt keine Mittel bieten. Und der geringste Wunsch, etwas im Navigationsprozess zu ändern, ermöglicht es Ihnen, das Projekt ohne Aufwand zu kompilieren, und es stürzt mit einem Knall in der Laufzeit ab, ohne die geringste Warnung von Xcode. Hier ist so ein WYSIWYG am Ende stellte sich heraus. Was Sie sehen, ist was Sie bekommen.


Sie können lange über den Charme dieser grauen Pfeile in Storyboards streiten, die angeblich jemandem die Verbindungen zwischen den Bildschirmen zeigen, aber wie meine Praxis gezeigt hat, habe ich absichtlich mehrere bekannte Entwickler aus verschiedenen Unternehmen interviewt, sobald das Projekt über 5-6 Bildschirme hinaus gewachsen ist, haben die Leute es versucht fand eine zuverlässigere Lösung und begann schließlich, die Struktur des Stapels von View-Controllern in meinem Kopf zu behalten. Und wenn Unterstützung für das iPad und andere Navigationsmodelle oder Unterstützung für Pushs hinzugefügt wurde, war dort alles traurig.


Seitdem wurden mehrere Versuche unternommen, um dieses Problem zu lösen, von denen einige zu separaten Frameworks führten, andere zu separaten Architekturmustern, da die Erstellung von Ansichtscontrollern innerhalb des Ansichtscontrollers dieses massive und ungeschickte Stück Code noch mehr machte.


Kehren wir zum Koordinatormuster zurück. Aus offensichtlichen Gründen finden Sie die Beschreibung nicht auf Wikipedia, da es sich nicht um ein Standardprogrammier- / Entwurfsmuster handelt. Es handelt sich vielmehr um eine Art Abstraktion, die vorschlägt, all diesen „hässlichen“ Code unter der Haube zu verstecken, um einen neuen Controller-Twist auf dem Stack zu erstellen und einzufügen, Links zum Controller-Container zu speichern und Daten zwischen Controllern zu übertragen. Den am besten geeigneten Artikel, der diesen Prozess beschreibt, würde ich einen Artikel auf raywenderlich.com nennen . Es wird nach der NSSpain-Konferenz 2015 populär, als die breite Öffentlichkeit darüber informiert wurde. Was hier gesagt wurde, finden Sie hier und hier .


Ich werde kurz beschreiben, woraus es besteht, bevor ich weitermache.


Das Koordinatormuster in allen Interpretationen passt ungefähr in dieses Bild:



Das heißt, der Koordinator ist ein Protokoll


 protocol Coordinator { func start() } 

Und der ganze hässliche Code soll in der Startfunktion versteckt sein. Der Koordinator kann außerdem Links zu untergeordneten Koordinatoren haben, dh sie können kompositorisch erstellt werden, und Sie können beispielsweise eine Implementierung durch eine andere ersetzen. Das heißt, es klingt ziemlich elegant.


Die Irrtümer beginnen jedoch ziemlich bald:


  1. Einige Implementierungen schlagen vor, den Koordinator von einem bestimmten Generierungsmuster in etwas Vernünftigeres zu verwandeln, den Stapel von Controllern zu beobachten und ihn zu einem Delegaten des Containers zu machen , z. B. UINavigationController , um das Klicken auf die Schaltfläche Zurück oder das Zurückwischen und Löschen des UINavigationController Koordinators zu verarbeiten. Aus natürlichen Gründen kann nur ein Objekt ein Delegat sein, was die Kontrolle über den Container selbst einschränkt und dazu führt, dass diese Logik entweder beim Koordinator liegt oder die Notwendigkeit schafft, diese Logik weiter unten an jemanden weiter unten in der Liste zu delegieren.
  2. Oft hängt die Logik zum Erstellen des nächsten Controllers von der Geschäftslogik ab . Um beispielsweise zum nächsten Bildschirm zu gelangen, muss der Benutzer am System angemeldet sein. Dies ist eindeutig ein asynchroner Prozess, der das Generieren eines Zwischenbildschirms mit dem Anmeldeformular umfasst. Der Anmeldevorgang selbst kann erfolgreich beendet werden oder nicht. Um zu vermeiden, dass der Koordinator in einen Massive Coordinator (ähnlich dem Massive View Controller) umgewandelt wird, müssen wir ihn zerlegen. Das heißt, Sie müssen einen Koordinator-Koordinator erstellen.
  3. Ein weiteres Problem für Koordinatoren besteht darin, dass sie im Wesentlichen Wrapper für Container-View-Controller wie UINavigationController , UITabBarController usw. sind. Und jemand sollte Links zu diesen Controllern bereitstellen. Wenn bei Kinderkoordinatoren alles noch weniger klar ist, dann ist bei den Anfangskoordinatoren der Kette nicht alles so einfach. Wenn Sie die Navigation ändern, beispielsweise für den A / B-Test, führt das Refactoring und die Anpassung solcher Koordinatoren zu separaten Kopfschmerzen. Besonders wenn sich der Containertyp ändert.
  4. All dies wird noch komplizierter, wenn die Anwendung externe Ereignisse unterstützt , die View Controller generieren. Zum Beispiel Push-Benachrichtigungen oder universelle Links (der Benutzer klickt auf den Link im Brief und fährt im entsprechenden Anwendungsbildschirm fort). Hier ergeben sich andere Unsicherheiten, auf die das Koordinatormuster keine genaue Antwort hat. Sie müssen genau wissen, auf welchem ​​Bildschirm sich der Benutzer gerade befindet, um ihm den nächsten Bildschirm anzuzeigen, der von einem externen Ereignis angefordert wird.
    Das einfachste Beispiel ist eine Chat-Anwendung, die aus 3 Bildschirmen besteht - einer Chat-Liste, dem Chat selbst, der in die Navigation des Chat-Listen-Controllers verschoben wird, und dem modal angezeigten Einstellungsbildschirm. Der Benutzer kann sich auf einem dieser Bildschirme befinden, wenn er eine Push-Benachrichtigung erhält und darauf tippt. Und hier beginnt die Unsicherheit: Wenn er in der Chat-Liste ist, müssen Sie einen Chat mit diesem bestimmten Benutzer starten. Wenn er bereits im Chat ist, müssen Sie ihn wechseln. Wenn er bereits im Chat mit diesem Benutzer ist, tun Sie nichts und aktualisieren Sie ihn, wenn der Benutzer eingeschaltet ist Einstellungsbildschirm - anscheinend müssen Sie die vorherigen Schritte schließen und befolgen. Oder vielleicht nicht schließen und den Chat einfach modal über die Einstellungen anzeigen? Und wenn sich die Einstellungen auf einer anderen Registerkarte befinden und nicht modal? Diese if/else entweder über die Koordinatoren verteilt oder gehen in Form eines Stückes Spaghetti zu einem anderen Mega-Koordinator. Außerdem handelt es sich entweder um aktive Iterationen auf dem Ansichtsstapel der Controller und um den Versuch, festzustellen, wo sich der Benutzer gerade befindet, oder um den Versuch, eine Anwendung zu erstellen, die seinen Status überwacht. Dies ist jedoch keine einfache Aufgabe, die nur auf der Art des Ansichts-Controller-Stapels selbst basiert.
  5. Und die Kirsche auf dem Kuchen sind UIKit-Pannen . Ein triviales Beispiel: ein UITabBarController mit einem UINavigationController auf der zweiten Registerkarte mit einem anderen UIViewController . Der Benutzer auf der ersten Registerkarte verursacht ein bestimmtes Ereignis, bei dem die Registerkarte UINavigationController und ein anderer Ansichtscontroller in seinen UINavigationController . All dies muss in einer solchen Reihenfolge erfolgen. Wenn der Benutzer zuvor noch nie eine zweite Registerkarte geöffnet hat und UINavigationController auf dem viewDidLoad nicht aufgerufen wurde, viewDidLoad die push Methode nicht und hinterlässt nur eine undeutliche Nachricht in der Konsole. Das heißt, Koordinatoren können in diesem Beispiel nicht einfach zu Zuhörern von Ereignissen gemacht werden, sie müssen in einer bestimmten Reihenfolge arbeiten. Sie müssen also voneinander Kenntnis haben. Und dies widerspricht bereits der ersten Aussage des Koordinatormusters, dass die Koordinatoren nichts über die generierenden Koordinatoren wissen und nur mit den untergeordneten Koordinatoren verbunden sind. Und schränkt auch ihre Austauschbarkeit ein.

Diese Liste kann fortgesetzt werden, aber im Allgemeinen ist klar, dass das Koordinatormuster eine eher begrenzte, schlecht skalierbare Lösung ist. Wenn Sie es ohne rosa Brille betrachten, können Sie einen Teil der Logik, die normalerweise in massiven UIViewController , in eine andere Klasse UIViewController . Alle Versuche, mehr als nur eine generative Fabrik zu schaffen und dort andere Logik einzuführen, enden nicht gut.


Es ist erwähnenswert, dass es Bibliotheken gibt, die auf diesem Muster basieren und auf die eine oder andere Weise die oben genannten Nachteile teilweise abmildern können. Ich würde XCoordinator und RxFlow erwähnen .


Was haben wir getan


Nachdem wir an dem Projekt mitgewirkt hatten, das wir von einem anderen Team zur Unterstützung und Entwicklung mit den Koordinatoren und ihrem vereinfachten „Urgroßmutter“ -Router im VIPER- Architekturansatz erhalten hatten, kehrten wir zu dem Ansatz zurück, der im vorherigen großen Projekt unseres Unternehmens gut funktioniert hatte. Dieser Ansatz hat keinen Namen. Es liegt an der Oberfläche. In unserer Freizeit wurde es in eine separate RouteComposer- Bibliothek kompiliert, die die Koordinatoren vollständig ersetzte und sich als flexibler erwies.


Was ist dieser Ansatz? Um mich auf den Stapel (Baum) zu verlassen, drehe ich die Controller so, wie sie sind. Um keine unnötigen Entitäten zu erstellen, die überwacht werden müssen. Speichern oder verfolgen Sie keine Bedingungen.


Schauen wir uns die UIKit- Entitäten genauer an und versuchen herauszufinden, was wir unter dem Strich haben und womit wir arbeiten können:


  1. Der Controller-Stack ist ein Baum. Es gibt einen Root-View-Controller mit untergeordneten View-Controllern. Modal dargestellte View-Controller sind ein Sonderfall von untergeordneten View-Controllern, da sie auch eine Bindung zum generierten View-Controller haben. Es ist alles sofort verfügbar.
  2. Ich muss Entitäten von Controllern erstellen. Sie haben alle unterschiedliche Konstruktoren und können mit Xib-Dateien oder Storyboards erstellt werden. Sie haben unterschiedliche Eingabeparameter. Aber sie sind sich darin einig, dass sie geschaffen werden müssen. Hier können wir also das Factory- Muster verwenden, das weiß, wie der gewünschte Ansichts-Controller erstellt wird. Jede Fabrik lässt sich leicht mit umfassenden Unit-Tests abdecken und ist unabhängig von anderen.
  3. Wir unterteilen die View Controller in zwei Klassen: 1. Zeigen Sie einfach die Controller an, 2. Container View Controller (Container View Controller) . Container-View-Controller unterscheiden sich von normalen darin, dass sie untergeordnete View-Controller enthalten können - auch Container oder einfache. Solche View Controller sind UINavigationController verfügbar: UINavigationController , UITabBarController usw., können aber auch vom Benutzer erstellt werden. Wenn wir es ignorieren, finden wir die folgenden Eigenschaften in allen Containern: 1. Sie haben eine Liste aller Controller, die sie enthalten. 2. Ein oder mehrere Controller sind derzeit sichtbar. 3. Sie werden möglicherweise aufgefordert, einen dieser Controller sichtbar zu machen. Dies ist alles, was UIKit- Controller tun können . Sie haben nur verschiedene Methoden dafür. Es gibt aber nur 3 Aufgaben.
  4. Um einen werkseitig erstellten Ansichtscontroller einzubetten, UITabBarController.selectedViewController = ... die übergeordnete Ansichtsmethode des Controllers UINavigationController.pushViewController(...) , UITabBarController.selectedViewController = ... , UIViewController.present(...) usw. Möglicherweise stellen Sie fest, dass immer zwei Ansichtscontroller erforderlich sind, einer bereits auf dem Stapel und einer, der in den Stapel eingebettet werden muss. Wickeln Sie dies in einen Wrapper und nennen Sie es Aktion (Aktion) . Jede Aktion lässt sich leicht mit umfassenden Unit-Tests abdecken und ist unabhängig von den anderen.
  5. Aus dem oben Gesagten geht hervor, dass Sie mit vorbereiteten Entitäten die Konfigurationskette Factory -> Action -> Factory -> Action -> Factory erstellen und nach Abschluss einen Ansichtsbaum von Controllern beliebiger Komplexität erstellen können. Sie müssen nur den Einstiegspunkt angeben. Diese Eingabepunkte sind normalerweise entweder der rootViewController des UIWindow oder der aktuelle View Controller, der der extremste Zweig des Baums ist. Das heißt, eine solche Konfiguration ist korrekt geschrieben als: Starten von ViewController -> Aktion -> Factory -> ... -> Factory .
  6. Zusätzlich zur Konfiguration benötigen Sie eine Entität, die weiß, wie die bereitgestellte Konfiguration gestartet und erstellt wird. Wir werden es Router nennen . Es hat keinen Status, es enthält keine Links. Es verfügt über eine Methode, an die die Konfiguration übergeben wird, und führt die Konfigurationsschritte nacheinander aus.
  7. Fügen Sie dem Router Verantwortung hinzu, indem Sie der Konfigurationskette Interceptors- Klassen hinzufügen. Es gibt drei Arten von Abfangjägern: 1. Wird vor dem Start der Navigation gestartet. Wir entfernen die Aufgaben der Benutzerauthentifizierung im System und andere asynchrone Aufgaben im System. 2. Führen Sie zum Zeitpunkt der Erstellung des Ansichtscontrollers aus, um die Werte festzulegen. 3. Wird nach der Navigation und nach verschiedenen analytischen Aufgaben ausgeführt. Jede Entität kann leicht durch Komponententests abgedeckt werden und weiß nicht, wie sie in der Konfiguration verwendet wird. Sie hat nur eine Verantwortung und sie erfüllt sie. Das heißt, die Konfiguration für die komplexe Navigation sieht möglicherweise wie folgt aus: [Aufgabe vor der Navigation ...] -> ViewController starten -> Aktion -> (Factory + [ContextTask ...]) -> ... -> (Factory + [ContextTask ...]) -> [Post NavigationTask ...] . Das heißt, alle Aufgaben werden vom Router nacheinander ausgeführt, wobei wiederum kleine, leicht lesbare atomare Einheiten ausgeführt werden.
  8. Die letzte Aufgabe, die von der Konfiguration nicht gelöst werden kann, bleibt bestehen - dies ist der aktuelle Status der Anwendung. Was ist, wenn wir nicht die gesamte Konfigurationskette, sondern nur einen Teil davon erstellen müssen, weil der Benutzer sie teilweise übergeben hat? Diese Frage kann vom Tree of View Controller immer eindeutig beantwortet werden. Denn wenn ein Teil der Kette bereits aufgebaut ist, befindet er sich bereits im Baum. Dies bedeutet, dass der Router verstehen kann, welcher Teil der Kette fertiggestellt werden muss, wenn jede Fabrik in der Kette die Frage beantworten kann, ob sie gebaut wurde oder nicht. Dies ist natürlich nicht die Aufgabe der Factory, daher wird eine andere atomare Entität eingeführt - der Finder. Jede Konfiguration sieht folgendermaßen aus: [Aufgabe vor der Navigation ...] -> ViewController starten -> Aktion -> (Finder / Factory + [ContextTask ...]) -> ... -> (Finder / Factory + [ContextTask ...]) -> [Post NavigationTask ...] . Wenn der Router am Ende mit dem Lesen beginnt, teilt ihm einer der Finder mit, dass es bereits erstellt wurde, und der Router beginnt ab diesem Zeitpunkt, die Kette wieder aufzubauen. Wenn sich keiner von ihnen im Baum befindet, müssen Sie die gesamte Kette vom ursprünglichen Controller aus erstellen.
    Bild
  9. Die Konfiguration muss stark typisiert sein. Daher arbeitet jede Entität nur mit einem Typ von Controller-Ansicht, ein Typ von Daten und Konfiguration beruht vollständig auf der Fähigkeit von Swift, schnell mit zugeordneten Typen zu arbeiten. Wir wollen uns auf den Compiler verlassen, nicht auf die Laufzeit. Ein Entwickler kann die Eingabe absichtlich schwächen, aber nicht umgekehrt.

Ein Beispiel für eine solche Konfiguration:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble() 

Die oben beschriebenen Elemente decken die gesamte Bibliothek ab und beschreiben den Ansatz. Wir müssen nur noch die Kettenkonfigurationen bereitstellen, die der Router ausführt, wenn der Benutzer auf eine Schaltfläche klickt oder ein externes Ereignis eintritt. Wenn es sich um verschiedene Gerätetypen handelt, z. B. iPhone oder iPad, stellen wir mithilfe des Polymorphismus unterschiedliche Übergangskonfigurationen bereit. Wenn wir A / B-Tests haben, dasselbe. Wir müssen zum Zeitpunkt des Starts der Navigation nicht über den Status der Anwendung nachdenken. Wir müssen sicherstellen, dass die Konfiguration anfangs korrekt geschrieben ist, und wir sind sicher, dass der Router sie irgendwie erstellen wird.


Der beschriebene Ansatz ist komplizierter als eine bestimmte Abstraktion oder ein bestimmtes Muster, aber wir haben uns dem Problem noch nicht gestellt, wo es nicht ausreichen würde. Natürlich erfordert RouteComposer einige Studien und Kenntnisse darüber, wie es funktioniert. Dies ist jedoch viel einfacher als das Erlernen der Grundlagen von AutoLayout oder RunLoop. Keine höhere Mathematik.


Die Bibliothek sowie die Implementierung des ihr zur Verfügung gestellten Routers verwenden keine objektiven Tricks zur Laufzeit und folgen vollständig allen Cocoa Touch-Konzepten. Sie helfen nur dabei, den Kompositionsprozess in Schritte zu unterteilen und diese in der angegebenen Reihenfolge auszuführen. Die Bibliothek wird mit den iOS-Versionen 9 bis 12 getestet.


Weitere Details finden Sie in früheren Artikeln:
Zusammensetzung der UIViewController und Navigation zwischen ihnen (und nicht nur) / Geek Magazine
Konfigurationsbeispiele für UIViewController mit dem RouteComposer / Geek-Magazin


Vielen Dank für Ihre Aufmerksamkeit. Gerne beantworte ich Fragen in den Kommentaren.

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


All Articles