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:
ClassFinder
ne trouvera pas ProductViewController
et le routeur ClassFinder
NilFinder
ne trouvera jamais rien et le routeur NilFinder
UIViewController
retournera toujours le UIViewController
le plus UIViewController
de la pile.- Démarrer
UIViewController
trouvé, le routeur reviendra - Construit un
UINavigationController
à l'aide de `NavigationControllerFactory - L'affichera de manière modale en utilisant
GeneralAction.presentModally
ProductViewController
ProductViewController ProductViewControllerFactory
- Intègre le
ProductViewController
créé dans le précédent UINavigationController
aide de UINavigationController.pushToNavigation
- 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))
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()
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:
- Définissez
StackIteratingFinder
pour qu'il recherche uniquement dans les éléments visibles à l'aide de [.current, .visible] - 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. - 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é.