Exemples de configuration d'UIViewControllers utilisant RouteComposer

Dans un article précédent , j'ai parlé de l'approche que nous utilisons pour composer et naviguer entre les contrôleurs de vue dans plusieurs applications sur lesquelles je travaille, ce qui a abouti à une bibliothèque RouteComposer distincte. J'ai reçu beaucoup de commentaires agréables sur l'article précédent et quelques conseils pratiques, ce qui m'a incité à en écrire un autre qui expliquerait un peu plus comment configurer la bibliothèque. Sous la coupe, je vais essayer de distinguer certaines des configurations les plus courantes.



Comment le routeur analyse la configuration


Pour commencer, considérez comment le routeur analyse la configuration que vous avez écrite. Prenons l'exemple de l'article précédent:


let productScreen = StepAssembly(finder: ClassFinder(options: [.current, .visible]), factory: ProductViewControllerFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

Le routeur passera par la chaîne d'étapes en commençant par la toute première, jusqu'à ce qu'une des étapes (à l'aide du Finder fourni) notifie que l' UIViewController souhaité UIViewController déjà sur la pile. (Ainsi, par exemple, GeneralStep.current() garanti d'être présent dans la pile de vue du contrôleur) Ensuite, le routeur commencera à reculer le long de la chaîne d'étapes en créant les UIViewController requis en utilisant les UIViewController fournis et en les intégrant en utilisant les Action spécifiées. Grâce à la vérification de type même au stade de la compilation, le plus souvent, vous ne pourrez pas utiliser des UITabBarController.addTab incompatibles avec le Fabric fourni (c'est-à-dire que vous ne pourrez pas utiliser UITabBarController.addTab dans le contrôleur construit par NavigationControllerFactory ).


Si vous imaginez la configuration décrite ci-dessus, alors si vous n'avez qu'un certain ProductViewController à l'écran, les étapes suivantes seront effectuées:


  1. ClassFinder ne trouvera pas ProductViewController et le routeur ClassFinder
  2. NilFinder ne trouvera jamais rien et le routeur NilFinder
  3. UIViewController retournera toujours le UIViewController le plus UIViewController de la pile.
  4. Démarrer UIViewController trouvé, le routeur reviendra
  5. Construit un UINavigationController à l'aide de `NavigationControllerFactory
  6. L'affichera de manière modale en utilisant GeneralAction.presentModally
  7. ProductViewController ProductViewController ProductViewControllerFactory
  8. Intègre le ProductViewController créé dans le précédent UINavigationController aide de UINavigationController.pushToNavigation
  9. Terminer la navigation

NB: Il faut comprendre qu'en réalité il est impossible d'afficher un UINavigationController modale sans UIViewController intérieur. Par conséquent, les étapes 5 à 8 seront exécutées par le routeur dans un ordre légèrement différent. Mais vous ne devriez pas y penser. La configuration est décrite séquentiellement.


Une bonne pratique lors de l'écriture d'une configuration consiste à supposer que l'utilisateur peut être localisé n'importe où dans votre application pour le moment, et, soudain, reçoit un message push avec une demande pour accéder à l'écran que vous décrivez, et essayez de répondre à la question - "Comment l'application devrait-elle se comporter? ? "," Comment le Finder se comportera-t-il dans la configuration que je décris? ". Si toutes ces questions sont prises en compte, vous obtenez une configuration qui est garantie de montrer à l'utilisateur l'écran souhaité où qu'il soit. Et c'est la principale exigence pour les applications modernes des équipes impliquées dans le marketing et pour attirer (engager) les utilisateurs.


StackIteratingFinder et ses options:


Vous pouvez implémenter le concept Finder de la manière que vous jugez la plus acceptable. Cependant, le moyen le plus simple consiste à parcourir le graphique des contrôleurs de vue à l'écran. Pour simplifier cet objectif, la bibliothèque fournit StackIteratingFinder et diverses implémentations qui prendront en charge cette tâche. Il vous suffit de répondre à la question - est-ce l' UIViewController que vous attendez.


Afin d'influencer le comportement de StackIteratingFinder et de lui indiquer dans quelles parties du graphique (pile) je veux que les contrôleurs recherchent, vous pouvez spécifier une combinaison de SearchOptions lors de sa création. Et ils devraient s'attarder plus en détail:


  • current : le contrôleur de vue le plus haut de la pile. (Celui qui est le rootViewController l' UIWindow ou celui qui est affiché de façon modale tout en haut)
  • visible : si le UIViewController est un conteneur, regardez dans son UIViewController visible ah (par exemple: UINavigationController toujours un UIViewController visible, UISplitController peut en avoir un ou deux selon la façon dont il est présenté.)
  • UIViewController : dans le cas où UIViewController est un conteneur, recherchez dans tous ses UIViewController imbriqués (par exemple: parcourez tous les contrôleurs de vue de UINavigationController y compris celui visible)
  • presenting : Recherche également dans tous les UIViewController ah sous le UIViewController (s'il y en a bien sûr)
  • presented : Recherchez UIViewController pour celui fourni (pour StackIteratingFinder cette option n'a pas de sens, car elle commence toujours par le haut)

La figure suivante peut rendre l'explication ci-dessus plus évidente:


Je recommanderais de vous familiariser avec le concept de conteneurs dans un article précédent.


Exemple Si vous voulez que votre Finder recherche un AccountViewController dans la pile entière, mais seulement parmi les UIViewController visibles, alors ceci devrait être écrit comme ceci:


 ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting]) 

NB Si pour une raison quelconque les paramètres fournis seront peu nombreux - vous pouvez toujours écrire facilement votre implémentation du Finder a. Un exemple sera dans cet article.


Passons en fait aux exemples.


Exemples de configurations avec explications


J'ai un certain UIViewController , qui est le rootViewController UIWindow , et je veux le HomeViewController par un certain HomeViewController à la fin de la navigation:


 let screen = StepAssembly( finder: ClassFinder<HomeViewController, Any?>(), factory: XibFactory()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble() 

XibFactory chargera HomeViewController partir du fichier HomeViewController.xib de HomeViewController.xib


N'oubliez pas que si vous utilisez des implémentations abstraites de Finder et Factory en combinaison, vous devez spécifier le type de UIViewController et le contexte pour au moins une des entités - ClassFinder<HomeViewController, Any?>


Que se passe-t-il si, dans l'exemple ci-dessus, je remplace GeneralStep.root par GeneralStep.current ?


La configuration fonctionnera jusqu'à ce qu'elle soit appelée au moment où il y a un UIViewController modal sur l'écran. Dans ce cas, GeneralAction.replaceRoot ne pourra pas remplacer le contrôleur racine, car il y a un contrôleur modal au-dessus et le routeur signalera une erreur. Si vous souhaitez que cette configuration fonctionne de toute façon, vous devez expliquer au routeur que vous souhaitez que GeneralAction.replaceRoot soit appliqué spécifiquement à l' UIViewController racine. Ensuite, le routeur supprimera tous les UIViewControllers représentés de façon UIViewController et la configuration fonctionnera dans toutes les situations.


Je veux montrer un AccountViewController , s'il est toujours bien affiché, à l'intérieur de n'importe quel UINavigationController et qui est actuellement à l'écran quelque part (même si ce UINavigationController sous un UIViewController modal):


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleStep(ClassFinder<UINavigationController, Any?>(), NilFactory())) .from(GeneralStep.current()) .assemble() 

Que signifie NilFactory dans cette configuration? Par cela, vous dites au routeur que s'il ne trouve aucun UINavigationController à l'écran, vous ne voulez pas qu'il le crée et qu'il ne fasse rien dans ce cas. Soit dit en passant, puisqu'il s'agit de NilFactory , vous ne pouvez pas utiliser Action après.


Je veux afficher un AccountViewController , s'il n'est pas déjà affiché, à l'intérieur de n'importe quel UINavigationController et qui est actuellement quelque part sur l'écran, et s'il ne s'avère pas tel, créez-le et affichez-le de façon modale:


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.PushToNavigation()) .from(SwitchAssembly<UINavigationController, Any?>() .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible)) //   -    .assemble(default: { //      return ChainAssembly() .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() }) ).assemble() 

Je veux montrer UITabBarController avec des UITabBarController contenant HomeViewController et AccountViewController en les remplaçant par la racine actuelle:


 let tabScreen = SingleContainerStep( finder: ClassFinder(), factory: CompleteFactoryAssembly(factory: TabBarControllerFactory()) .with(XibFactory<HomeViewController, Any?>(), using: UITabBarController.addTab()) .with(XibFactory<AccountViewController, Any?>(), using: UITabBarController.addTab()) .assemble()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble() 

Puis-je utiliser le UIViewControllerTransitioningDelegate personnalisé avec l'action GeneralAction.presentModally :


 let transitionController = CustomViewControllerTransitioningDelegate() //     .using(GeneralAction.PresentModally(transitioningDelegate: transitionController)) 

Je veux accéder à AccountViewController , où que se trouve l'utilisateur, dans un autre onglet ou même dans une sorte de fenêtre modale:


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: NilFactory()) .from(tabScreen) .assemble() 

Pourquoi utilisons-nous NilFactory ? Nous n'avons pas besoin de construire un AccountViewController s'il n'est pas trouvé. Il sera construit dans la configuration tabScreen . Voir ci-dessus.


Je veux afficher de façon modale ForgotPasswordViewController , mais, certainement, après LoginViewController intérieur de UINavigationController :


 let loginScreen = StepAssembly( finder: ClassFinder<LoginViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() let forgotPasswordScreen = StepAssembly( finder: ClassFinder<ForgotPasswordViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(loginScreen.expectingContainer()) .assemble() 

Vous pouvez utiliser la configuration de l'exemple pour la navigation dans ForgotPasswordViewController et LoginViewController


Pourquoi expectingContainer dans l'exemple ci-dessus?


Étant donné que l'action pushToNavigation nécessite la présence d'un UINavigationController et dans la configuration suivante, la méthode expectingContainer nous permet d'éviter une erreur de compilation en veillant à ce que lorsque le routeur atteint loginScreen en cours d' loginScreen , le UINavigationController sera là.


Que se passe-t-il si dans la configuration ci-dessus je remplace GeneralStep.current par GeneralStep.root ?


Cela fonctionnera, mais puisque vous dites au routeur que vous voulez qu'il commence à construire une chaîne à partir du UIViewController racine, si des UIViewController modaux sont ouverts au-dessus, le routeur les UIViewController avant de commencer à construire la chaîne.


Mon application a un UITabBarController contenant des HomeViewController et BagViewController . Je veux que l'utilisateur puisse basculer entre eux en utilisant les icônes sur les onglets comme d'habitude. Mais si j'appelle la configuration par programme (par exemple, l'utilisateur clique sur "Aller au sac" à l'intérieur du HomeViewController ), l'application ne doit pas changer l'onglet, mais afficher le BagViewController modale.


Il existe 3 façons d'y parvenir dans la configuration:


  1. Définissez StackIteratingFinder pour qu'il recherche uniquement dans les éléments visibles à l'aide de [.current, .visible]
  2. Utilisez NilFinder ce qui signifie que le routeur ne trouvera jamais le BagViewController dans les BagViewController et le créera toujours. Cependant, cette approche a un effet secondaire - si, par exemple, un utilisateur déjà dans BagViewController est présenté de manière modale, et, par exemple, clique sur un lien universel que BagViewController devrait lui montrer, alors le routeur ne le trouvera pas et créera une autre instance et l'afficher au-dessus modalement. Ce n'est peut-être pas ce que vous voulez.
  3. Modifiez un peu ClassFinder afin qu'il ne trouve que le BagViewController affiché de façon modale et ignore le reste, et l'utilise déjà dans la configuration.

 struct ModalBagFinder: StackIteratingFinder { func isTarget(_ viewController: BagViewController, with context: Any?) -> Bool { return viewController.presentingViewController != nil } } let screen = StepAssembly( finder: ModalBagFinder(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

Au lieu d'une conclusion


J'espère que les méthodes de configuration du routeur sont devenues plus claires. Comme je l'ai dit, nous utilisons cette approche dans 3 applications et n'avons pas encore rencontré de situation où elle n'est pas assez flexible. La bibliothèque, ainsi que l'implémentation du routeur qui lui est fourni, n'utilise aucune astuce objective avec le runtime et suit entièrement tous les concepts Cocoa Touch, aidant uniquement à diviser le processus de composition en étapes et à les exécuter dans la séquence donnée et testées avec les versions iOS 9 à 12. En outre , cette approche s'intègre dans tous les modèles architecturaux qui impliquent de travailler avec la pile UIViewController (MVC, MVVM, VIP, RIB, VIPER, etc.)


Je serais heureux de vos commentaires et suggestions. Surtout si vous pensez qu'il vaut la peine de s'attarder sur certains aspects plus en détail. Peut-être que le concept de contextes doit être clarifié.

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


All Articles