
In diesem Artikel möchte ich die Erfahrung teilen, die wir seit mehreren Jahren erfolgreich in unseren iOS-Anwendungen nutzen, von denen sich 3 derzeit im Appstore befinden. Dieser Ansatz hat gut funktioniert, und wir haben ihn kürzlich vom Rest des Codes getrennt und in eine separate RouteComposer- Bibliothek umgewandelt, auf die noch näher eingegangen wird.
https://github.com/ekazaev/route-composer
Lassen Sie uns zunächst herausfinden, was unter der Zusammensetzung von View Controllern in iOS zu verstehen ist.
Bevor ich mit der Erklärung selbst UIViewController
ich Sie daran erinnern, dass es in iOS am häufigsten als View Controller oder UIViewController
. Dies ist eine Klasse, die vom Standard- UIViewController
geerbt wurde. Dies ist der Basis-MVC-Pattern-Controller, den Apple für die Entwicklung von iOS-Anwendungen empfiehlt.
Sie können alternative Architekturmuster wie MVVM, VIP, VIPER verwenden, aber in diesen wird der UIViewController
auf die eine oder andere Weise einbezogen, was bedeutet, dass diese Bibliothek mit ihnen verwendet werden kann. Die Essenz des UIViewController
verwendet, um das UIView
zu steuern, das am häufigsten einen Bildschirm oder einen wesentlichen Teil des Bildschirms darstellt, Ereignisse daraus verarbeitet und einige Daten darin anzeigt.

Alle UIViewController
können bedingt in Normal View Controller unterteilt werden , die für einen sichtbaren Bereich auf dem Bildschirm verantwortlich sind, und Container View Controller , die neben sich selbst und einigen ihrer Steuerelemente auch auf die eine oder andere Weise in sie integrierte UIViewController
View Controller anzeigen können .
Zu den mit Cocoa Touch gelieferten Standard-Controllern für die Containeransicht gehören: UINavigationConroller
, UITabBarController
, UISplitController
, UIPageController
und einige andere. Außerdem kann der Benutzer gemäß den in der Apple-Dokumentation beschriebenen Cocoa Touch-Regeln eigene benutzerdefinierte Controller für die Containeransicht erstellen.
Der Prozess der Einführung von Standard-View-Controllern in die Container-View-Controller sowie die Integration von View-Controllern in den Controller-Stack werden in diesem Artikel als Zusammensetzung bezeichnet.
Warum sich dann herausstellte, dass die Standardlösung für die Zusammensetzung von Ansichtssteuerungen für uns nicht optimal war, und wir eine Bibliothek entwickelten, die unsere Arbeit erleichtert.
Schauen wir uns als Beispiel die Zusammensetzung einiger Standard-Controller für die Containeransicht an:
Beispiele für die Zusammensetzung in Standardbehältern
UINavigationController

let tableViewController = UITableViewController(style: .plain)
UITabBarController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController()
UISplitViewController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController()
Beispiele für die Integration (Zusammensetzung) von Ansichtscontrollern auf dem Stapel
Installieren des View Controller-Stamms
let window: UIWindow =
Modale Darstellung des View Controllers
window.rootViewController.present(splitViewController, animated: animated, completion: nil)
Warum wir beschlossen haben, eine Bibliothek für Kompositionen zu erstellen
Wie Sie den obigen Beispielen entnehmen können, gibt es keine einzige Möglichkeit, herkömmliche Ansichtssteuerungen in Container zu integrieren, so wie es keine einzige Möglichkeit gibt, einen Stapel von Ansichtssteuerungen zu erstellen. Wenn Sie das Layout Ihrer Anwendung oder die Art und Weise, wie Sie darin navigieren, geringfügig ändern möchten, müssen Sie den Anwendungscode erheblich ändern. Außerdem benötigen Sie Links zu Containerobjekten, damit Sie Ihre Ansichtssteuerungen in diese einfügen können. Das heißt, die Standardmethode selbst erfordert einen relativ großen Arbeitsaufwand sowie das Vorhandensein von Links zum Anzeigen von Controllern zum Generieren von Aktionen und Präsentationen anderer Controller.
All dies bereitet den verschiedenen Methoden zum Verknüpfen mit der Anwendung Kopfschmerzen (z. B. mithilfe von Universal-Links), da Sie die Frage beantworten müssen: Was ist, wenn der Controller dem Benutzer angezeigt werden muss, da er bereits auf den Link in der Safari geklickt hat, oder wenn ich den Controller ansehe? Dies sollte zeigen, dass es noch nicht erstellt wurde , und Sie dazu zwingen, durch den Baum der Ansichts-Controller zu gehen und Code zu schreiben, vor dem manchmal Ihre Augen zu bluten beginnen und den jeder iOS-Entwickler zu verbergen versucht. Im Gegensatz zur Android-Architektur, bei der jeder Bildschirm separat erstellt wird, muss in iOS möglicherweise ein ziemlich großer Stapel von Controllern erstellt werden, der unter dem von Ihnen auf Anfrage angezeigten Controller ausgeblendet wird, um einen Teil der Anwendung unmittelbar nach dem Start anzuzeigen.
Es wäre großartig, nur Methoden wie goToAccount()
, goToMenu()
oder goToProduct(withId: "012345")
wenn ein Benutzer auf eine Schaltfläche klickt oder wenn eine Anwendung goToProduct(withId: "012345")
universellen Link von einer anderen Anwendung goToProduct(withId: "012345")
und nicht daran denkt, diesen Ansichtscontroller in den Stapel zu integrieren dass der Ersteller dieses View Controllers diese Implementierung bereits bereitgestellt hat.
Darüber hinaus bestehen unsere Anwendungen häufig aus einer großen Anzahl von Bildschirmen, die von verschiedenen Teams entwickelt wurden. Um während des Entwicklungsprozesses zu einem der Bildschirme zu gelangen, müssen Sie einen anderen Bildschirm durchlaufen, der möglicherweise noch nicht erstellt wurde. In unserer Firma haben wir den Ansatz verwendet, den wir Petrischale nennen. Das heißt, im Entwicklungsmodus haben Entwickler und Tester Zugriff auf eine Liste aller Anwendungsbildschirme und können zu jedem von ihnen wechseln (natürlich erfordern einige von ihnen möglicherweise einige Eingabeparameter).

Sie können mit ihnen interagieren und einzeln testen und sie dann zur endgültigen Anwendung für die Produktion zusammenfügen. Dieser Ansatz erleichtert die Entwicklung erheblich, aber wie Sie aus den obigen Beispielen gesehen haben, beginnt die Kompositionshölle, wenn Sie den Code auf verschiedene Arten im Code behalten müssen, um den Ansichts-Controller in den Stapel zu integrieren.
Es bleibt hinzuzufügen, dass all dies mit N multipliziert wird, sobald Ihr Marketingteam den Wunsch äußert, A / B-Tests an Live-Benutzern durchzuführen und zu überprüfen, welche Navigationsmethode besser funktioniert, z. B. eine Registerkartenleiste oder ein Hamburger-Menü.
Lass uns Susanins Beine schneiden Lassen Sie uns 50% der Benutzer der Registerkartenleiste und des anderen Hamburger-Menüs anzeigen. In einem Monat werden wir Ihnen mitteilen, welche Benutzer mehr von unseren Sonderangeboten sehen.
Ich werde versuchen, Ihnen zu sagen, wie wir die Lösung für dieses Problem angegangen sind und sie schließlich der RouteComposer-Bibliothek zugewiesen haben.
Susanin Routenkomponist
Nachdem wir alle Kompositions- und Navigationsszenarien analysiert hatten, versuchten wir, den in den obigen Beispielen angegebenen Code zu abstrahieren, und identifizierten drei Hauptentitäten, von denen die RouteComposer- Bibliothek arbeitet - Factory
, Finder
, Action
. Darüber hinaus enthält die Bibliothek drei zusätzliche Entitäten, die für eine kleine RoutingInterceptor
verantwortlich sind, die möglicherweise während des Navigationsprozesses erforderlich ist - RoutingInterceptor
, ContextTask
, PostRoutingTask
. Alle diese Entitäten müssen in einer Kette von Abhängigkeiten konfiguriert und an Router
y übertragen werden, das Objekt, das Ihren Controller-Stapel erstellt.
Aber über jeden von ihnen in der Reihenfolge:
Fabrik
Wie der Name schon sagt, ist Factory
für die Erstellung des View Controllers verantwortlich.
public protocol Factory { associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws -> ViewController }
Hier ist es wichtig, Vorbehalte gegen das Konzept des Kontextes zu machen . Den Kontext innerhalb der Bibliothek nennen wir alles, was der Betrachter benötigt, um erstellt zu werden. Um beispielsweise einen Ansichts-Controller anzuzeigen, der Produktdetails anzeigt, müssen Sie eine bestimmte Produkt-ID übergeben, z. B. in Form eines Strings. Die Essenz des Kontexts kann alles sein: ein Objekt, eine Struktur, ein Block oder ein Tupel. Wenn Ihr Controller zum Erstellen nichts benötigt, kann der Kontext als Any?
angegeben werden Any?
und in nil
installieren.
Zum Beispiel:
class ProductViewControllerFactory: Factory { func build(with productID: UUID) throws -> ProductViewController { let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID
Aus der obigen Implementierung wird deutlich, dass diese Factory das Controller-Image aus der XIB-Datei lädt und die übertragene Produkt-ID darin installiert. Zusätzlich zum Standard- Factory
Protokoll bietet die Bibliothek mehrere Standardimplementierungen dieses Protokolls, um zu verhindern, dass Sie banalen Code schreiben (insbesondere das obige Beispiel).
Außerdem werde ich keine Beschreibungen von Protokollen und Beispiele für deren Implementierung bereitstellen, da Sie sich mit ihnen im Detail vertraut machen können, indem Sie das mit der Bibliothek gelieferte Beispiel herunterladen. Es gibt verschiedene Implementierungen von Fabriken für herkömmliche Ansichtssteuerungen und Container sowie Möglichkeiten zu deren Konfiguration.
Aktion
Die Action
beschreibt, wie ein Ansichtscontroller, der von der Factory erstellt wird, in den Stapel integriert wird. Der Ansichtscontroller kann nach der Erstellung nicht einfach in der Luft hängen. Daher sollte jede Fabrik eine Action
enthalten, wie aus dem obigen Beispiel ersichtlich.
Die häufigste Implementierung von Action
ist die modale Darstellung des Controllers:
class PresentModally: Action { func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: ActionResult) -> Void) { guard existingController.presentedViewController == nil else { completion(.failure("\(existingController) is already presenting a view controller.")) return } existingController.present(viewController, animated: animated, completion: { completion(.continueRouting) }) } }
Die Bibliothek enthält die Implementierung der meisten Standardmethoden zum Integrieren von Ansichtscontrollern in den Stapel, und Sie müssen wahrscheinlich erst dann eigene erstellen, wenn Sie eine Art benutzerdefinierten Containeransichtscontroller oder eine Präsentationsmethode verwenden. Das Erstellen benutzerdefinierter Aktionen sollte jedoch keine Probleme verursachen, wenn Sie die Beispiele lesen.
Finder
Die Essenz von Finder
beantwortet den Router auf die Frage : Ist ein solcher Controller bereits erstellt und befindet er sich bereits auf dem Stapel? Vielleicht muss nichts erstellt werden und es reicht zu zeigen, was bereits da ist? .
public protocol Finder { associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) -> ViewController? }
Wenn Sie Links zu allen von Ihnen erstellten Ansichtscontrollern speichern, können Sie in Ihrer Finder
Implementierung einfach einen Link zum gewünschten Ansichtscontroller zurückgeben. Meistens ist dies jedoch nicht der Fall, da sich der Anwendungsstapel, insbesondere wenn er groß ist, sehr dynamisch ändert. Darüber hinaus können mehrere identische Ansichtscontroller auf dem Stapel unterschiedliche Entitäten anzeigen (z. B. mehrere ProductViewController, die unterschiedliche Produkte mit unterschiedlichen Produkt-IDs anzeigen). Daher erfordert die Finder
Implementierung möglicherweise eine benutzerdefinierte Implementierung und die Suche nach dem entsprechenden Ansichtscontroller auf dem Stapel. Die Bibliothek erleichtert diese Aufgabe, indem sie den StackIteratingFinder
als Erweiterung des Finder
StackIteratingFinder
, ein Protokoll mit den entsprechenden Einstellungen, um diese Aufgabe zu vereinfachen. Bei der Implementierung von StackIteratingFinder
Sie nur die Frage beantworten: StackIteratingFinder
dieser Ansichts-Controller derjenige, nach dem der Router auf Ihre Anfrage sucht StackIteratingFinder
Ein Beispiel für eine solche Implementierung:
class ProductViewControllerFinder: StackIteratingFinder { let options: SearchOptions init(options: SearchOptions = .currentAndUp) { self.options = options } func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool { return productViewController.productID == productID } }
Helferentitäten
RoutingInterceptor
RoutingInterceptor
können Sie einige Aktionen ausführen, bevor Sie mit der Komposition von Ansichtscontrollern beginnen, und dem Router mitteilen, ob es möglich ist, Ansichtscontroller in den Stapel zu integrieren. Das häufigste Beispiel für eine solche Aufgabe ist die Authentifizierung (in der Implementierung jedoch überhaupt nicht üblich). Sie möchten beispielsweise einen Ansichtscontroller mit den Details eines Benutzerkontos anzeigen, dafür muss der Benutzer jedoch am System angemeldet sein. Sie können einen RoutingInterceptor
implementieren und zur Konfiguration der Ansicht des Benutzerdetail-Controllers und der RoutingInterceptor
hinzufügen: Wenn der Benutzer angemeldet ist, lassen Sie den Router die Navigation fortsetzen. Wenn nicht, zeigen Sie den View-Controller an, der den Benutzer zur Anmeldung auffordert. Wenn diese Aktion erfolgreich ist, lassen Sie den Router die Navigation fortsetzen oder abbrechen sie, wenn der Benutzer sich weigert, sich anzumelden.
class LoginInterceptor: RoutingInterceptor { func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) -> Void) { guard !LoginManager.sharedInstance.isUserLoggedIn else {
Eine Implementierung eines solchen RoutingInterceptor
mit Kommentaren ist in dem mit der Bibliothek gelieferten Beispiel enthalten.
ContextTask
Die ContextTask
, sofern Sie sie bereitstellen, separat auf jeden Ansichtscontroller in der Konfiguration angewendet werden, unabhängig davon, ob sie gerade von einem Router erstellt wurde oder auf dem Stapel gefunden wurde. Sie möchten lediglich die darin enthaltenen Daten aktualisieren und / oder einige Standarddaten festlegen Parameter (z. B. Schaltfläche zum Schließen anzeigen oder nicht anzeigen).
PostRoutingTask
Die Implementierung von PostRoutingTask
wird vom Router nach erfolgreichem Abschluss der Integration des angeforderten View Controllers in den Stack aufgerufen. Bei der Implementierung ist es bequem, verschiedene Analysen hinzuzufügen oder verschiedene Dienste abzurufen.
Ausführlichere Informationen zur Implementierung aller beschriebenen Entitäten finden Sie in der Dokumentation zur Bibliothek sowie im beigefügten Beispiel.
PS: Die Anzahl der Hilfsentitäten, die der Konfiguration hinzugefügt werden können, ist nicht begrenzt.
Konfiguration
Alle beschriebenen Entitäten sind insofern gut, als sie den Kompositionsprozess in kleine, austauschbare und vertrauenswürdige Blöcke aufteilen.
Kommen wir nun zum Wichtigsten - zur Konfiguration, dh zur Verbindung dieser Blöcke miteinander. Um diese Blöcke untereinander zu sammeln und zu einer Kette von Schritten zu kombinieren, bietet die Bibliothek eine Builder-Klasse StepAssembly
(für Container - ContainerStepAssembly
). Mit seiner Implementierung können Sie die Kompositionsblöcke wie Perlen auf einer Zeichenfolge in ein einzelnes Konfigurationsobjekt einfügen und die Abhängigkeiten von den Konfigurationen anderer Ansichtssteuerungen angeben. Was in Zukunft mit der Konfiguration zu tun ist, liegt bei Ihnen. Sie können es dem Router mit den erforderlichen Parametern zuführen und es wird ein Stapel von Controllern für Sie erstellt. Sie können es im Wörterbuch speichern und später per Schlüssel verwenden - dies hängt von Ihrer spezifischen Aufgabe ab.
Stellen Sie sich ein triviales Beispiel vor: Angenommen, durch Klicken auf eine Zelle in der Liste oder wenn die Anwendung einen universellen Link von einem Safari- oder E-Mail-Client erhält, müssen wir den Produktcontroller mit einer bestimmten Produkt-ID modal anzeigen. In diesem Fall muss der Produktcontroller in den UINavigationController
damit er seinen Namen und die Schaltfläche zum Schließen auf dem Bedienfeld UINavigationController
kann. Darüber hinaus kann dieses Produkt nur angemeldeten Benutzern angezeigt werden. Andernfalls laden Sie sie zur Anmeldung ein.
Wenn Sie dieses Beispiel analysieren, ohne eine Bibliothek zu verwenden, sieht es ungefähr so aus:
class ProductArrayViewController: UITableViewController { let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance
Dieses Beispiel beinhaltet nicht die Implementierung universeller Links, die das Isolieren des Autorisierungscodes und das Beibehalten des Kontexts erfordern, nach dem der Benutzer geleitet werden soll, sowie das Suchen, wenn der Benutzer plötzlich auf einen Link klickt, und dieses Produkt wird ihm bereits gezeigt, was den Code letztendlich sehr macht schwer zu lesen.
Betrachten Sie die Konfiguration dieses Beispiels mithilfe der Bibliothek:
let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory())
Wenn Sie dies in die menschliche Sprache übersetzen:
- Überprüfen Sie, ob der Benutzer angemeldet ist, und bieten Sie ihm gegebenenfalls eine Eingabe an
- Wenn sich der Benutzer erfolgreich angemeldet hat, fahren Sie fort
- Suchen Sie nach dem von
Finder
bereitgestellten Product View Controller - Wenn es gefunden wurde - sichtbar machen und fertig
- Wenn es nicht gefunden wurde - erstellen Sie einen
UINavigationController
, integrieren Sie den von ProductViewControllerFactory
mithilfe von ProductViewControllerFactory
erstellten View Controller in ihn GenericActions.PresentModally
UINavigationController
mit GenericActions.PresentModally
aus dem aktuellen Ansichtscontroller ein
Die Konfiguration erfordert einige Studien, wie viele komplexe Lösungen, z. B. das Konzept von AutoLayout
und erscheint auf den ersten Blick kompliziert und redundant. Die Anzahl der Aufgaben, die mit dem angegebenen Codefragment gelöst werden müssen, deckt jedoch alle Aspekte von der Autorisierung bis zur Tiefenverknüpfung ab. Durch das Aufteilen in eine Abfolge von Aktionen kann die Konfiguration einfach geändert werden, ohne dass Änderungen am Code vorgenommen werden müssen. Darüber hinaus können Sie mit der Implementierung von StepAssembly
Probleme mit einer unvollständigen StepAssembly
der StepAssembly
vermeiden - Probleme mit der Inkompatibilität von Eingabeparametern für verschiedene Ansichtssteuerungen.
Betrachten Sie den Pseudocode einer vollständigen Anwendung, in der ein ProductArrayViewController
eine Liste von Produkten anzeigt. Wenn der Benutzer dieses Produkt auswählt, wird es angezeigt, je nachdem, ob der Benutzer angemeldet ist oder nicht, oder er bietet an, sich anzumelden, und wird nach erfolgreicher Anmeldung angezeigt:
Konfigurationsobjekte
class ProductArrayViewController: UITableViewController { let products: [UUID]?
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
.
. , , , — ProductArrayViewController, UINavigationController HomeViewController — StepAssembly
from()
. RouteComposer
, ( ). , Configuration
. , A/B , .
Anstelle einer Schlussfolgerung
, 3 . , , . Fabric
, Finder
Action
. , — , , . , .
, , objective c Cocoa Touch, . iOS 9 12.
UIViewController
(MVC, MVVM, VIP, RIB, VIPER ..)
, , , . . .
.