Coordinateur d'applications dans les applications iOS

Chaque année, la plate-forme iOS subit de nombreux changements.En outre, les bibliothèques tierces travaillent régulièrement sur le travail avec le réseau, la mise en cache des données, le rendu de l'interface utilisateur via JavaScript, etc. Contrairement à toutes ces tendances, Pavel Gurov a parlé de la solution architecturale, qui sera pertinente quelles que soient les technologies que vous utilisez actuellement ou utiliserez dans quelques années.

ApplicationCoordinator peut être utilisé pour créer une navigation entre les écrans, et en même temps résoudre un certain nombre de problèmes. Sous la démonstration de chat et les instructions pour la mise en œuvre la plus rapide de cette approche.



À propos de l'orateur: Pavel Gurov développe des applications iOS dans Avito.



La navigation





La navigation entre les écrans est une tâche à laquelle 100% d'entre vous sont confrontés, peu importe ce que vous faites - un réseau social, un taxi ou une banque en ligne. C’est ce que commence l’application même au stade du prototype, lorsque vous ne savez même pas à quoi ressembleront les écrans, à quel type d’animations ils ressembleront, si les données seront mises en cache. Les écrans peuvent être des images vides ou statiques, mais la tâche de navigation apparaît dans l'application dès qu'il y a plusieurs de ces écrans . C'est, presque immédiatement.



Les méthodes les plus courantes pour construire l'architecture des applications iOS: MVc, MVVm et MVp, décrivent comment construire un module à écran unique. Il indique également que les modules peuvent se connaître, communiquer entre eux, etc. Mais très peu d'attention est accordée aux questions de savoir comment les transitions entre ces modules sont effectuées, qui décide de ces transitions et comment les données sont transmises.

UlStoryboard + segues


iOS prêt à l'emploi fournit plusieurs façons d'afficher le scénario d'écran suivant:

  1. Le célèbre UlStoryboard + enchaîne , lorsque nous désignons toutes les transitions entre les écrans dans un même métafichier, puis les appelons. Tout est très pratique et super.
  2. Conteneurs - tels que UINavigationController. UITabBarController, UIPageController ou, éventuellement, des conteneurs auto-écrits qui peuvent être utilisés à la fois par programme et avec StoryBoards.
  3. Méthode présente (_: animée: achèvement :). Ceci est juste une méthode de la classe UIController.

Il n'y a aucun problème avec ces outils eux-mêmes. Le problème est exactement comment ils sont couramment utilisés. UINavigationController, performSegue, prepareForSegue, la méthode presentViewController sont toutes des méthodes de propriété de la classe UIViewController. Apple suggère d'utiliser ces outils dans l'UIViewController lui-même.



La preuve en est la suivante.



Ce sont des commentaires qui apparaissent dans votre projet si vous créez une nouvelle sous-classe de UIViewController à l'aide d'un modèle standard. Il est écrit directement - si vous utilisez des séquences et que vous devez transférer des données vers l'écran suivant selon le scénario, vous devez: obtenir ce ViewController de séquence; savoir de quel type il s'agit; cassez-le sur ce type et passez-y vos données.

Cette approche des problèmes de construction de la navigation.

1. Connectivité rigide des écrans

Cela signifie que l'écran 1 connaît l'existence de l'écran 2. Non seulement il connaît son existence, mais il peut également le créer ou le retirer de la séquence, en sachant de quel type il s'agit et lui transférer des données.

Si nous devons, dans certaines circonstances, afficher l'écran 3 au lieu de l'écran 2, alors nous devrons connaître le nouvel écran 3 de la même manière pour coudre dans le contrôleur d'écran 1. Tout devient encore plus difficile si les contrôleurs 2 et 3 peuvent être appelés à partir de plusieurs autres endroits, non seulement de l'écran 1. Il s'avère que la connaissance des écrans 2 et 3 devra être cousue dans chacun de ces endroits.

Pour ce faire, c'est une autre moitié du problème, les principaux problèmes commenceront lorsqu'il sera nécessaire d'apporter des modifications à ces transitions ou de prendre en charge tout cela.



2. Réorganiser les contrôleurs de script

Ce n'est pas aussi simple à cause de la connexion. Pour échanger deux ViewControllers, il ne suffira pas d'aller dans l'UlStoryboard et d'échanger 2 images. Vous devrez ouvrir le code de chacun de ces écrans, le transférer dans les paramètres du suivant et changer ses emplacements, ce qui n'est pas très pratique.



3. Transfert de données selon le scénario

Par exemple, lorsque vous choisissez quelque chose sur l'écran 3, nous devons mettre à jour la vue sur l'écran 1. Comme nous n'avons initialement rien d'autre qu'un ViewController, nous devrons en quelque sorte connecter les deux ViewControllers - peu importe comment - par délégation ou d'une manière ou d'une autre encore. Ce sera encore plus difficile si, selon l'action de l'écran 3, il faudra mettre à jour non pas un écran, mais plusieurs à la fois, par exemple le premier et le second.



Dans ce cas, la délégation ne peut être dispensée, car la délégation est une relation un à un. Quelqu'un dira, utilisons la notification, quelqu'un - à travers un état partagé. Tout cela rend difficile le débogage et le suivi des flux de données dans notre application.

Comme on dit, il vaut mieux voir une fois qu'entendre 100 fois. Regardons un exemple spécifique de cette application Avito Services Pro. Cette application est destinée aux professionnels du secteur des services, dans lesquels il est pratique de suivre vos commandes, de communiquer avec les clients, de rechercher de nouvelles commandes.

Scénario - choisir une ville lors de la modification d'un profil utilisateur.



Voici un écran d'édition de profil, tel est le cas dans de nombreuses applications. Nous voulons choisir une ville.

Que se passe-t-il ici?

  • L'utilisateur clique sur la cellule avec la ville et le premier écran décide qu'il est temps d'ajouter l'écran suivant à la pile de navigation. Il s'agit d'un écran avec une liste des villes fédérales (Moscou et Saint-Pétersbourg) et une liste des régions.
  • Si l'utilisateur sélectionne une ville fédérale sur le deuxième écran, le deuxième écran comprend que le script est terminé, transmet la ville sélectionnée au premier et la pile de navigation revient au premier écran. Le script est considéré comme complet.
  • Si l'utilisateur sélectionne une zone sur le deuxième écran, le deuxième écran décide qu'un troisième écran doit être préparé, dans lequel nous voyons une liste des villes de cette zone. Si l'utilisateur sélectionne une ville, cette ville est envoyée au premier écran, lance la pile de navigation et le script est considéré comme terminé.

Dans ce diagramme, les problèmes de connectivité que j'ai mentionnés précédemment sont représentés sous forme de flèches entre le ViewController. Nous allons maintenant nous débarrasser de ces problèmes.

Comment fait-on cela?

  1. Nous nous interdisons dans UIViewController d'accéder aux conteneurs , c'est-à-dire à self.navigationController, self.tabBarController ou à d'autres conteneurs personnalisés que vous avez créés comme extension de propriété. Maintenant, nous ne pouvons pas prendre notre conteneur du code d'écran et lui demander de faire quelque chose.


  2. Nous nous interdisons à l'intérieur de l'UIViewController d'appeler la méthode performSegue et d'écrire du code dans la méthode prepareForSegue, ce qui prendrait l'écran qui suit le script et le configurerait. Autrement dit, nous ne travaillons plus avec segue (avec des transitions entre les écrans) à l'intérieur de l'UIViewController.


  3. Nous interdisons également toute mention d'autres contrôleurs à l'intérieur de notre contrôleur spécifique : pas d'initialisation, de transfert de données, et c'est tout.




Coordinateur


Puisque nous supprimons toutes ces responsabilités de l'UIViewController, nous avons besoin d'une nouvelle entité qui les exécutera. Créez une nouvelle classe d'objets et appelez-la coordinatrice.



Le coordinateur est juste un objet ordinaire auquel nous passons au début du NavigationController et appelons la méthode Start. Maintenant, ne pensez pas à la façon dont elle est mise en œuvre, regardez simplement comment le scénario de choix d'une ville va changer dans ce cas.

Maintenant, cela ne commence pas par le fait que nous préparons la transition vers un écran NavigationController spécifique, mais nous appelons la méthode Start au niveau du coordinateur, en la passant avant celle-ci dans l'initialiseur NavigationController. Le coordinateur comprend qu'il est temps pour le NavigationController de lancer le premier écran, ce qu'il fait.

De plus, lorsque l'utilisateur sélectionne une cellule avec une ville, cet événement est transmis au coordinateur. Autrement dit, l'écran lui-même ne sait rien - après, comme on dit, au moins une inondation. Il envoie ce message au coordinateur, puis le coordinateur réagit à cela par (puisqu'il a un NavigationController), qui lui envoie l'étape suivante - c'est le choix des régions.

Ensuite, l'utilisateur clique sur "Région" - la même image exacte - l'écran lui-même ne résout rien, indique seulement au coordinateur que l'écran suivant s'ouvre.

Lorsque l'utilisateur sélectionne une ville spécifique sur le troisième écran, cette ville est également transférée vers le premier écran via le coordinateur. Autrement dit, un message est envoyé au coordinateur qu'une ville a été sélectionnée. Le coordinateur envoie ce message au premier écran et fait rouler la pile de navigation vers le premier écran.

Notez que les contrôleurs ne communiquent plus entre eux , décidant qui sera le prochain, et ne se transmettent aucune donnée. De plus, ils ne savent rien de leur environnement.



Si nous considérons l'application dans le cadre d'une architecture à trois couches, le ViewController devrait idéalement s'intégrer complètement dans la couche Présentation et porter le moins possible la logique d'application.

Dans ce cas, nous utilisons le coordinateur pour extraire la logique des transitions vers la couche ci-dessus et supprimer ces connaissances du ViewController.

Démo


Une présentation et un projet de démonstration sont disponibles sur Github, ci-dessous est une démonstration pendant la conférence.


C'est le même scénario: éditer un profil et y choisir une ville.

Le premier écran est l'écran d'édition de l'utilisateur. Il affiche des informations sur l'utilisateur actuel: nom et ville sélectionnée. Il y a un bouton "Choisissez une ville". Lorsque nous cliquons dessus, nous arrivons à l'écran avec une liste de villes. Si nous sélectionnons une ville là-bas, le premier écran obtient cette ville.

Voyons maintenant comment cela fonctionne dans le code. Commençons par le modèle.

struct City { let name: String } struct User { let name: String var city: City? } 

Les modèles sont simples:

  1. Une structure de ville qui a un nom de champ, une chaîne;
  2. Un utilisateur qui a également un nom et une ville de propriété.

Ensuite, StoryBoard . Cela commence par un NavigationController. En principe, voici les mêmes écrans qui étaient dans le simulateur: un écran d'édition utilisateur avec une étiquette et un bouton et un écran avec une liste de villes, qui montre une tablette avec des villes.

Écran d'édition de l'utilisateur


 import UIKit final class UserEditViewController: UIViewController, UpdateableWithUser { // MARK: - Input - var user: User? { didSet { updateView() } } // MARK: - Output - var onSelectCity: (() -> Void)? @IBOutlet private weak var userLabel: UILabel? @IBAction private func selectCityTap(_ sender: UIButton) { onSelectCity?() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateView() } private func updateView() { userLabel?.text = "User: \(user?.name ?? ""), \n" + "City: \(user?.city?.name ?? "")" } } 

Ici, il y a une propriété User - c'est l'utilisateur qui est transmis à l'extérieur - l'utilisateur que nous éditerons. Définir ici l'utilisateur provoque l'appel du bloc didSet, ce qui conduit à un appel à la méthode locale updateView (). Tout ce que cette méthode fait, c'est simplement mettre des informations sur l'utilisateur sur l'étiquette, c'est-à-dire montrer son nom et le nom de la ville dans laquelle cet utilisateur vit.

La même chose se produit dans la méthode viewWillAppear ().

L'endroit le plus intéressant est le gestionnaire de clic sur le bouton de sélection de la ville selectCityTap (). Ici, le contrôleur lui-même ne résout rien : il ne crée aucun contrôleur, il n'appelle aucune séquence. Tout ce qu'il fait est un rappel - c'est la deuxième propriété de notre ViewController. Le rappel onSelectCity n'a pas de paramètres. Lorsque l'utilisateur clique sur le bouton, cela provoque l'appel de ce rappel.

Écran de sélection de la ville


 import UIKit final class CitiesViewController: UITableViewController { // MARK: - Output - var onCitySelected: ((City) -> Void)? // MARK: - Private variables - private let cities: [City] = [City(name: "Moscow"), City(name: "Ulyanovsk"), City(name: "New York"), City(name: "Tokyo")] // MARK: - Table - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cities.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = cities[indexPath.row].name return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { onCitySelected?(cities[indexPath.row]) } } 

Cet écran est un UITableViewController. La liste des villes ici est fixe, mais elle peut provenir d'ailleurs. De plus (// MARK: - Table -) est un code de table assez trivial qui affiche une liste de villes dans les cellules.

L'endroit le plus intéressant ici est le gestionnaire didSelectRowAt IndexPath, une méthode bien connue pour tout le monde. Ici encore, l'écran lui-même ne résout rien. Que se passe-t-il une fois la ville sélectionnée? Il appelle simplement un rappel avec un seul paramètre "ville".

Ceci termine le code des écrans eux-mêmes. On le voit, ils ne savent rien de leur environnement.

Coordinateur


Passons au lien entre ces écrans.

 import UIKit protocol UpdateableWithUser: class { var user: User? { get set } } final class UserEditCoordinator { // MARK: - Properties private var user: User { didSet { updateInterfaces() } } private weak var navigationController: UINavigationController? // MARK: - Init init(user: User, navigationController: UINavigationController) { self.user = user self.navigationController = navigationController } func start() { showUserEditScreen() } // MARK: - Private implementation private func showUserEditScreen() { let controller = UIStoryboard.makeUserEditController() controller.user = user controller.onSelectCity = { [weak self] in self?.showCitiesScreen() } navigationController?.pushViewController(controller, animated: false) } private func showCitiesScreen() { let controller = UIStoryboard.makeCitiesController() controller.onCitySelected = { [weak self] city in self?.user.city = city _ = self?.navigationController?.popViewController(animated: true) } navigationController?.pushViewController(controller, animated: true) } private func updateInterfaces() { navigationController?.viewControllers.forEach { ($0 as? UpdateableWithUser)?.user = user } } } 

Le coordinateur a deux propriétés:

  1. Utilisateur - l'utilisateur que nous modifierons;
  2. NavigationController auquel transmettre au démarrage.

Il existe un simple init () qui remplit ces propriétés.

Vient ensuite la méthode start (), qui provoque l' appel de la méthode ShowUserEditScreen () . Arrêtons-nous dessus plus en détail. Cette méthode prend le contrôleur hors de UIStoryboard, le transmet à notre utilisateur local. Ensuite, il met le rappel SelectCity et pousse ce contrôleur dans la pile de navigation.

Une fois que l'utilisateur a cliqué sur le bouton, le rappel onSelectCity est déclenché, ce qui provoque l' appel de la méthode ShowCitiesScreen () privée suivante.

En fait, cela fait presque la même chose - il soulève un contrôleur légèrement différent de l'UIStoryboard, place le rappel onCitySelected dessus et le pousse dans la pile de navigation - c'est tout ce qui se passe. Lorsque l'utilisateur sélectionne une ville spécifique, ce rappel est déclenché, le coordinateur met à jour le champ «ville» de notre utilisateur local et fait rouler la pile de navigation vers le premier écran.

Puisque l'utilisateur est une structure, la mise à jour du champ "ville" dans celui-ci conduit au fait que le bloc didSet est appelé, respectivement, la méthode privée updateInterfaces () est appelée. Cette méthode parcourt toute la pile de navigation et essaie de déployer chaque ViewController en tant que protocole UpdateableWithUser. Il s'agit du protocole le plus simple, qui n'a qu'une seule propriété - utilisateur. Si cela réussit, il le jette à l'utilisateur mis à jour. Ainsi, il s'avère que notre utilisateur sélectionné sur le deuxième écran passe automatiquement au premier écran.

Tout est clair avec le coordinateur, et la seule chose qui reste à montrer ici est le point d'entrée de notre candidature. C'est là que tout commence. Dans ce cas, il s'agit de la méthode didFinishLaunchingWithOptions de notre AppDelegate.

 import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var coordinator: UserEditCoordinator! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { guard let navigationController = window?.rootViewController as? UINavigationController else { return true } let user = User(name: "Pavel Gurov", city: City(name: "Moscow")) coordinator = UserEditCoordinator(user: user, navigationController: navigationController) coordinator.start() return true } } 

Ici, le navigationController est tiré de l'UIStoryboard, un utilisateur est créé, que nous éditerons, avec un nom et une ville spécifique. Ensuite, nous créons notre coordinateur avec User et navigationController. Il appelle la méthode start (). Le coordinateur est transféré à la propriété locale - c'est essentiellement tout. Le schéma est assez simple.

Entrées et sorties


Il y a plusieurs points sur lesquels je voudrais m'attarder plus en détail. Vous avez probablement remarqué que la propriété dans userEditViewController est marquée avec un commentaire comme entrée et les rappels de ces contrôleurs sont marqués comme sortie.



Une entrée est une donnée qui peut changer avec le temps, ainsi que certaines méthodes ViewController qui peuvent être appelées de l'extérieur. Par exemple, dans UserEditViewController, il s'agit d'une propriété User - l'utilisateur lui-même ou son paramètre City peut changer.

Une sortie est tout événement que le contrôleur veut communiquer avec le monde extérieur. Dans UserEditViewController, il s'agit d'un clic sur le bouton onSelectCity, et sur l'écran de sélection de la ville, il s'agit d'un clic sur une cellule avec une ville spécifique. L'idée principale ici est, je le répète, que le contrôleur ne sait rien et ne fait rien à propos de ces événements. Il délègue ce qu'il faut faire à quelqu'un d'autre.

Dans Objective-C, je n'aimais pas vraiment écrire des rappels de sauvegarde en raison de leur syntaxe horrible. Mais dans Swift, c'est beaucoup plus simple. L'utilisation de rappels dans ce cas est une alternative au modèle de délégation bien connu dans iOS. Seulement ici, au lieu de désigner des méthodes dans le protocole et de dire que le coordinateur correspond à ce protocole, puis d'écrire ces méthodes quelque part séparément, nous pouvons immédiatement créer une entité en un seul endroit, y mettre un rappel et tout faire.

Certes, avec cette approche, contrairement à la délégation, il existe un lien étroit entre l'essence du coordinateur et l'écran, car le coordinateur sait qu'il existe une essence spécifique de l'écran.

Vous pouvez vous en débarrasser de la même manière qu'en délégation, en utilisant des protocoles.



Pour éviter la connectivité, nous pouvons fermer l' entrée et la sortie de notre contrôleur avec un protocole .

Ci-dessus, le protocole CitiesOutput, qui a exactement une exigence - le rappel onCitySelected. À gauche est un analogue de ce schéma sur Swift. Notre contrôleur se conforme à ce protocole, déterminant le rappel nécessaire. Nous le faisons pour que le coordinateur ne connaisse pas l'existence de la classe CitiesViewController. Mais à un moment donné, il devra configurer la sortie de ce contrôleur. Afin de lancer le tout, nous ajoutons une usine au coordinateur.



L'usine a une méthode cityOutput (). Il s'avère que notre coordinateur ne crée pas de contrôleur et ne l'obtient pas de quelque part. Une usine le lui lance, ce qui renvoie un objet fermé par le protocole dans la méthode, et il ne sait rien de la classe de cet objet.

Maintenant, la chose la plus importante - pourquoi faire tout cela? Pourquoi devons-nous intégrer un autre niveau supplémentaire alors qu'il n'y a eu aucun problème de toute façon?

On peut imaginer cette situation: un manager viendra nous voir et vous demandera de faire un test A / B du fait qu'au lieu d'une liste de villes nous aurions le choix d'une ville sur la carte. Si dans notre application, le choix de la ville n'était pas au même endroit, mais dans différents coordinateurs, dans différents scénarios, nous devions coudre un drapeau à chaque endroit, le jeter à l'extérieur, sur ce drapeau, lever l'un ou l'autre ViewController. Ce n'est pas très pratique.

Nous voulons retirer ces connaissances du coordinateur. Par conséquent, on pourrait le faire en un seul endroit. Dans l'usine elle-même, nous ferions un paramètre par lequel l'usine retourne l'un ou l'autre contrôleur fermé par le protocole. Les deux auraient un rappel sur CitySelected, et le coordinateur, en principe, ne se soucierait pas avec lequel de ces écrans travailler - une carte ou une liste.

Composition VS héritage


Le point suivant sur lequel je voulais m'arrêter est la composition contre l'héritage.



  1. La première méthode pour faire notre coordinateur est de faire la composition lorsque le NavigationController lui est passé de l'extérieur et stocké localement en tant que propriété. C'est comme une composition - nous y avons ajouté un NavigationController comme propriété.
  2. D'un autre côté, il y a une opinion que tout est là dans le kit d'interface utilisateur, et nous n'avons pas besoin de réinventer la roue. Vous pouvez simplement prendre et hériter de l'UI NavigationController .

Chaque option a ses avantages et ses inconvénients, mais personnellement, il me semble que la composition dans ce cas est plus appropriée que l'héritage. L'hérédité est généralement un régime moins flexible. Si nous devons, par exemple, changer la navigation en, disons, UIPageController, alors dans le premier cas, nous pouvons simplement les fermer avec un protocole commun, comme «Afficher l'écran suivant» et substituer commodément le conteneur dont nous avons besoin.

De mon point de vue, l'argument le plus important est que vous cachez à l'utilisateur final dans la composition toutes les méthodes inutiles. Il s'avère qu'il est moins susceptible de trébucher. Vous ne laissez que l' API nécessaire , par exemple, la méthode Start - et c'est tout. Il n'a aucun moyen d'appeler la méthode PushViewController, PopViewController, c'est-à-dire d'interférer d'une manière ou d'une autre avec les activités du coordinateur. Toutes les méthodes de la classe parente sont masquées.

Storyboards


Je pense qu'ils méritent une attention particulière ainsi que des séquences. Personnellement, je soutiens les séquences , car elles vous permettent de vous familiariser rapidement avec le script. Lorsqu'un nouveau développeur arrive, il n'a pas besoin de grimper le code, les storyboards aident à cela. Même si vous créez une interface avec le code, vous pouvez laisser le ViewController vide et créer l'interface avec le code, mais laissez au moins les transitions et l'intégralité du point. L'essence même des storyboards se trouve dans les transitions elles-mêmes, et non dans la disposition de l'interface utilisateur.

Heureusement, l' approche coordinatrice ne limite pas le choix des outils . Nous pouvons utiliser en toute sécurité des coordinateurs avec des séquences. Mais nous devons nous rappeler que maintenant nous ne pouvons pas travailler avec des séquences à l'intérieur de l'UIViewController.



Par conséquent, nous devons remplacer la méthode onPrepareForSegue dans notre classe. Au lieu de faire quelque chose à l'intérieur du contrôleur, nous déléguerons à nouveau ces tâches au coordinateur, via le rappel. La méthode onPrepareForSegue est appelée, vous ne faites rien vous-même - vous ne savez pas de quel type de transition il s'agit, de quel contrôleur de destination il s'agit - cela n'a pas d'importance pour vous. Vous venez de jeter tout cela dans un rappel, et le coordinateur le découvrira. Il a cette connaissance, vous n'avez pas besoin de cette connaissance.

Afin de rendre tout plus simple, vous pouvez le faire dans une certaine classe de base afin de ne pas la remplacer dans chaque contrôleur pris séparément. Dans ce cas, il sera plus pratique pour le coordinateur de travailler avec vos séquences.

Une autre chose que je trouve pratique avec le Storyboard est d'adhérer à la règle selon laquelle un Storyboard est égal à un coordinateur . Ensuite, vous pouvez tout simplifier considérablement, créer une classe en général - StoryboardCoordinator, et y générer le paramètre RootType, créer le contrôleur de navigation initial dans le Storyboard et envelopper le script entier dedans.



Comme vous pouvez le voir, ici le coordinateur a 2 propriétés: navigationController; Le rootViewController de notre RootType est générique. Lors de l'initialisation, nous lui passons non pas un navigationController spécifique, mais un Storyboard, à partir duquel notre navigation racine et son premier contrôleur obtiennent. De cette façon, nous n'aurons même pas à appeler de méthode Start. Autrement dit, vous avez créé un coordinateur, il a immédiatement Navigation et a immédiatement Root. Vous pouvez soit afficher la navigation de façon modale, soit prendre racine et pousser dans la navigation existante et continuer à travailler.

Dans ce cas, notre UserEditCoordinator deviendrait simplement des typealias, en remplaçant le type de son RootViewController dans le paramètre générique.

Transfert de données de script en arrière


Parlons de la résolution du dernier problème, que j'ai décrit au début. Il s'agit du transfert de données vers le script.



Considérez le même scénario pour choisir une ville, mais maintenant il sera possible de choisir non pas une ville, mais plusieurs. Afin de montrer à l'utilisateur qu'il a sélectionné plusieurs villes dans la même région, nous afficherons à l'écran avec une liste de régions un petit nombre à côté du nom de la région, indiquant le nombre de villes sélectionnées dans cette région.

Il s'avère que l'action sur un contrôleur (sur le troisième) devrait entraîner une modification de l'apparence de plusieurs autres à la fois. Autrement dit, dans le premier, nous devons afficher dans la cellule avec la ville, et dans le second, nous devons mettre à jour tous les numéros dans les régions sélectionnées.

Le coordinateur simplifie cette tâche en retransférant les données vers le script - il s'agit désormais d'une tâche aussi simple que de transférer les données vers l'avant conformément au script.

Que se passe-t-il ici? L'utilisateur sélectionne une ville. Ce message est envoyé au coordinateur. Le coordinateur, comme je l'ai déjà montré dans la démo, parcourt toute la pile de navigation et envoie des données mises à jour à toutes les parties intéressées. En conséquence, ViewController peut mettre à jour sa vue avec ces données.

Refactorisation du code existant


Comment refactoriser le code existant si vous souhaitez intégrer cette approche dans une application existante qui a MVc, MVVm ou MVp?



Vous avez un tas de ViewController. La première chose à faire est de les diviser en scénarios auxquels ils participent. Dans notre exemple, il existe 3 scénarios: autorisation, modification de profil, bande.



Nous enveloppons maintenant chaque scénario à l'intérieur de notre coordinateur. Nous devrions en fait pouvoir démarrer ces scripts depuis n'importe où dans notre application. Cela devrait être flexible - le coordinateur doit être complètement autosuffisant .

Cette approche de développement offre une commodité supplémentaire. Il consiste en ce que si vous travaillez actuellement avec un scénario spécifique, vous n'avez pas besoin de cliquer dessus à chaque démarrage. Vous pouvez le démarrer rapidement au début, y modifier quelque chose, puis supprimer ce démarrage temporaire.

Après avoir décidé de nos coordinateurs, nous devons déterminer quel scénario peut conduire au démarrage d'un autre et créer un arbre à partir de ces scénarios.



Dans notre cas, l'arborescence est simple: LoginCoordinator peut démarrer le coordinateur d'édition de profil. Ici, presque tout se met en place, mais un détail très important demeure - notre schéma n'a pas de point d'entrée.



Ce point d'entrée sera un coordinateur spécial - ApplicationCoordinator . Il est créé et démarré par AppDelegate , puis il contrôle déjà la logique au niveau de l'application, c'est-à-dire que le coordinateur démarre en ce moment.

Nous avons juste regardé un circuit très similaire, seulement il y avait ViewController au lieu de coordinateurs, et nous l'avons fait pour que ViewController ne se sache rien et ne se transmette pas de données. En principe, la même chose peut être faite avec les coordinateurs. Nous pouvons désigner une certaine entrée (méthode Start) et une sortie (rappel onFinish) en eux. Les coordinateurs deviennent indépendants, réutilisables et facilement testables . Les coordinateurs cessent de se connaître et ne communiquent, par exemple, qu'avec ApplicationCoordinator.

Vous devez être prudent, car si votre application a suffisamment de ces scripts, alors ApplicationCoordinator peut se transformer en un énorme objet divin, il connaîtra tous les scripts existants - ce n'est pas très cool non plus. Ici, nous devons déjà regarder - peut-être diviser les coordinateurs en sous-coordinateurs, c'est-à-dire réfléchir à une telle architecture afin que ces objets ne grandissent pas à des tailles incroyables. Bien que la taille ne soit pas toujours un motif de refactoring .

Par où commencer


Je conseille de commencer par le bas - implémentez d'abord des scripts individuels.



Comme solution de contournement, ils peuvent être démarrés dans UIViewController. Autrement dit, tant que vous n'avez pas Root ou d'autres coordinateurs, vous pouvez créer un coordinateur et, en tant que solution temporaire, le démarrer à partir de UIViewController, en l'enregistrant localement dans la propriété (comme nextCoordinator est ci-dessus). Lorsqu'un événement se produit, vous, comme je l'ai montré dans la démonstration, créez une propriété locale, placez-y le coordinateur et appelez la méthode Start dessus. Tout est très simple.

Ensuite, lorsque tous ces coordinateurs ont déjà terminé, le début de l'un à l'intérieur de l'autre est exactement le même. Avez-vous une propriété locale ou une sorte de tableau de dépendances comme coordinateur, vous mettez tout cela là-dedans pour qu'il ne fonctionne pas et appelez la méthode Start.

Résumé


  • Les écrans et scripts indépendants qui ne se connaissent pas ne communiquent pas entre eux. Nous avons essayé d'y parvenir.
  • Il est facile de changer l'ordre des écrans dans l'application sans changer les codes d'écran. Si tout est fait comme il se doit, la seule chose qui devrait changer dans l'application lorsque le script change n'est pas le code écran, mais le code coordinateur.
  • Transfert de données simplifié entre les écrans et autres tâches qui impliquent une connexion entre les écrans.
  • Personnellement, mon moment préféré est que pour commencer à l'utiliser, vous n'avez pas besoin d'ajouter de dépendances tierces au projet et de comprendre le code des autres.

AppsConf 2018 est déjà le 8 et 9 octobre - ne le manquez pas! Étudiez plutôt le calendrier (ou révisez- le) et réservez vos billets. Naturellement, une grande attention est accordée aux deux plates-formes - iOS et Android, ainsi que des rapports sur l'architecture qui ne sont pas liés à une seule technologie, et une discussion sur d'autres questions importantes liées au monde autour du développement mobile.

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


All Articles