
Dans cet article, je veux partager l'expérience que nous utilisons avec succès depuis plusieurs années dans nos applications iOS, dont 3 sont actuellement dans l'Appstore. Cette approche a bien fonctionné et nous l'avons récemment séparée du reste du code et l'avons conçue dans une bibliothèque RouteComposer distincte, qui sera discutée en fait .
https://github.com/ekazaev/route-composer
Mais, pour commencer, essayons de comprendre ce que l'on entend par composition des contrôleurs de vue dans iOS.
Avant de passer à l'explication elle-même, je vous rappelle que dans iOS, il est le plus souvent compris comme un contrôleur de vue ou UIViewController
. Il s'agit d'une classe héritée du standard UIViewController
, qui est le contrôleur de modèle MVC de base qu'Apple recommande d'utiliser pour développer des applications iOS.
Vous pouvez utiliser des modèles architecturaux alternatifs tels que MVVM, VIP, VIPER, mais en eux l' UIViewController
sera impliqué d'une manière ou d'une autre, ce qui signifie que cette bibliothèque peut être utilisée avec eux. L'essence de l' UIViewController
utilisée pour contrôler l' UIView
, qui représente le plus souvent un écran ou une partie importante de l'écran, en traite les événements et y affiche des données.

Tous les UIViewController
peuvent être conditionnellement divisés en contrôleurs de vue normale , qui sont responsables d'une zone visible sur l'écran, et contrôleurs de vue de conteneur , qui, en plus de s'afficher eux-mêmes et certains de leurs contrôles, peuvent également afficher des contrôleurs de vue enfant intégrés en eux d'une manière ou d'une autre. .
Les contrôleurs de vue de conteneur standard fournis avec Cocoa Touch comprennent: UINavigationConroller
, UITabBarController
, UISplitController
, UIPageController
et quelques autres. De plus, l'utilisateur peut créer ses propres contrôleurs de vue de conteneur personnalisés en suivant les règles Cocoa Touch décrites dans la documentation Apple.
Le processus d'introduction des contrôleurs de vue standard dans les contrôleurs de vue de conteneur, ainsi que l'intégration des contrôleurs de vue dans la pile des contrôleurs, nous appellerons la composition dans cet article.
Pourquoi, alors, la solution standard pour la composition des contrôleurs de vue s'est avérée ne pas être optimale pour nous, et nous avons développé une bibliothèque qui facilite notre travail.
Jetons un coup d'œil à la composition de certains contrôleurs de vue de conteneur standard à titre d'exemple:
Exemples de composition dans des conteneurs standard
UINavigationController

let tableViewController = UITableViewController(style: .plain)
UITabBarController

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

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController()
Exemples d'intégration (composition) de contrôleurs de vue sur la pile
Installation de la racine du contrôleur de vue
let window: UIWindow =
Présentation modale du contrôleur de vue
window.rootViewController.present(splitViewController, animated: animated, completion: nil)
Pourquoi nous avons décidé de créer une bibliothèque de composition
Comme vous pouvez le voir dans les exemples ci-dessus, il n'y a pas de moyen unique d'intégrer des contrôleurs de vue conventionnels dans des conteneurs, tout comme il n'y a pas de moyen unique de créer une pile de contrôleurs de vue. Et, si vous souhaitez modifier légèrement la disposition de votre application ou la façon dont vous y naviguez, vous aurez besoin de modifications importantes du code de l'application, vous aurez également besoin de liens vers des objets conteneur afin que vous puissiez y insérer vos contrôleurs de vue, etc. Autrement dit, la méthode standard elle-même implique une quantité de travail assez importante, ainsi que la présence de liens vers des contrôleurs de vue pour générer des actions et des présentations d'autres contrôleurs.
Tout cela ajoute un casse-tête à diverses méthodes de liaison par dip vers l'application (par exemple, en utilisant des liens universels), car vous devez répondre à la question: que se passe-t-il si le contrôleur doit être montré à l'utilisateur car il a cliqué sur le lien dans le safari est déjà affiché, ou je regarde le contrôleur ce qui devrait montrer qu'il n'a pas encore été créé , vous obligeant à parcourir l'arborescence des contrôleurs de vue et à écrire du code dont parfois vos yeux commencent à saigner et que tout développeur iOS essaie de cacher. De plus, contrairement à l'architecture Android où chaque écran est construit séparément, dans iOS, afin d'afficher une partie de l'application immédiatement après le lancement, il peut être nécessaire de créer une assez grande pile de contrôleurs qui sera masquée sous celle que vous montrez sur demande.
Il serait goToAccount()
appeler des méthodes telles que goToAccount()
, goToMenu()
ou goToProduct(withId: "012345")
lorsqu'un utilisateur clique sur un bouton ou lorsqu'une application goToProduct(withId: "012345")
lien universel d'une autre application et ne songe pas à intégrer ce contrôleur de vue dans la pile, sachant que le créateur de ce contrôleur de vue a déjà fourni cette implémentation.
De plus, souvent, nos applications se composent d'un grand nombre d'écrans développés par différentes équipes, et pour accéder à l'un des écrans pendant le processus de développement, vous devez passer par un autre écran qui n'a peut-être pas encore été créé. Dans notre entreprise, nous avons utilisé l'approche que nous appelons la boîte de Pétri . Autrement dit, en mode développement, le développeur et le testeur ont accès à une liste de tous les écrans d'application et il peut accéder à chacun d'entre eux (bien sûr, certains d'entre eux peuvent nécessiter certains paramètres d'entrée).

Vous pouvez interagir avec eux et tester individuellement, puis les assembler dans l'application finale pour la production. Cette approche facilite grandement le développement, mais, comme vous l'avez vu dans les exemples ci-dessus, l'enfer de la composition commence lorsque vous devez conserver dans le code plusieurs façons d'intégrer le contrôleur de vue dans la pile.
Reste à ajouter que tout cela sera multiplié par N dès que votre équipe marketing exprime le souhait de réaliser des tests A / B sur des utilisateurs live et de vérifier quelle méthode de navigation fonctionne mieux, par exemple, une barre d'onglets ou un menu hamburger?
Coupons les jambes de Susanin Montrons 50% des utilisateurs de la barre d'onglets, et à l'autre menu Hamburger, et dans un mois, nous vous dirons quels utilisateurs voient le plus de nos offres spéciales?
Je vais essayer de vous dire comment nous avons abordé la solution à ce problème et l'avons finalement allouée à la bibliothèque RouteComposer.
Susanin Compositeur de route
Après avoir analysé tous les scénarios de composition et de navigation, nous avons essayé d'abstraire le code donné dans les exemples ci-dessus et identifié 3 entités principales dont la bibliothèque RouteComposer fonctionne - Factory
, Finder
, Action
. De plus, la bibliothèque contient 3 entités auxiliaires qui sont responsables d'un petit réglage qui peut être requis pendant le processus de navigation - RoutingInterceptor
, ContextTask
, PostRoutingTask
. Toutes ces entités doivent être configurées dans une chaîne de dépendances et transférées vers le Router
y, l'objet qui construira votre pile de contrôleurs.
Mais, à propos de chacun d'eux dans l'ordre:
Usine
Comme son nom l'indique, Factory
est responsable de la création du contrôleur de vue.
public protocol Factory { associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws -> ViewController }
Ici, il est important de faire une réserve sur la notion de contexte . Le contexte au sein de la bibliothèque, nous appelons tout ce dont le spectateur peut avoir besoin pour être créé. Par exemple, afin d'afficher un contrôleur de vue qui affiche les détails du produit, vous devez lui passer un certain productID, par exemple, sous la forme d'une String
. L'essence du contexte peut être n'importe quoi: un objet, une structure, un bloc ou un tuple. Si votre contrôleur n'a besoin de rien pour être créé - le contexte peut-il être spécifié comme Any?
et installer en nil
.
Par exemple:
class ProductViewControllerFactory: Factory { func build(with productID: UUID) throws -> ProductViewController { let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID
De la mise en œuvre ci-dessus, il devient clair que cette usine chargera l'image du contrôleur à partir du fichier XIB et y installera l'ID produit transféré. En plus du protocole Factory
standard, la bibliothèque fournit plusieurs implémentations standard de ce protocole afin de vous éviter d'écrire du code banal (en particulier, l'exemple ci-dessus).
De plus, je m'abstiendrai de fournir des descriptions de protocoles et des exemples de leurs implémentations, car vous pouvez vous familiariser avec eux en détail en téléchargeant l'exemple fourni avec la bibliothèque. Il existe différentes implémentations d'usines pour les contrôleurs de vue et conteneurs conventionnels, ainsi que des moyens de les configurer.
Action
L'entité Action
est une description de la façon d'intégrer un contrôleur de vue, qui sera construit par l'usine, sur la pile. Le contrôleur de vue après la création ne peut pas simplement rester en l'air et, par conséquent, chaque usine doit contenir une Action
comme le montre l'exemple ci-dessus.
La mise en œuvre la plus courante d' Action
est la présentation modale du contrôleur:
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) }) } }
La bibliothèque contient l'implémentation de la plupart des façons standard d'intégrer des contrôleurs de vue dans la pile, et vous n'aurez probablement pas à créer le vôtre jusqu'à ce que vous utilisiez une sorte de contrôleur de vue de conteneur personnalisé ou une méthode de présentation. Mais la création d'actions personnalisées ne devrait pas poser de problème si vous lisez les exemples.
Finder
L'essence de Finder
répond au routeur à la question - Un tel contrôleur est-il déjà créé et est-il déjà sur la pile? Peut-être que rien n'est nécessaire pour être créé et suffit-il de montrer ce qui existe déjà? .
public protocol Finder { associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) -> ViewController? }
Si vous stockez des liens vers tous les contrôleurs de vue que vous avez créés, dans votre implémentation du Finder
vous pouvez simplement renvoyer un lien vers le contrôleur de vue souhaité. Mais le plus souvent, ce n'est pas le cas, car la pile d'applications, surtout si elle est volumineuse, change de manière assez dynamique. De plus, vous pouvez avoir plusieurs contrôleurs de vue identiques sur la pile montrant différentes entités (par exemple, plusieurs ProductViewControllers montrant différents produits avec différents ID de produit), de sorte que l'implémentation du Finder
peut nécessiter une implémentation personnalisée et rechercher le contrôleur de vue correspondant sur la pile. La bibliothèque facilite cette tâche en fournissant le StackIteratingFinder
comme une extension du Finder
, un protocole avec les paramètres appropriés pour simplifier cette tâche. Dans l'implémentation de StackIteratingFinder
vous n'avez qu'à répondre à la question - est-ce que ce contrôleur de vue est celui que le routeur recherche à votre demande.
Un exemple d'une telle implémentation:
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 } }
Entités auxiliaires
RoutingInterceptor
RoutingInterceptor
vous permet d'effectuer certaines actions avant de commencer la composition des contrôleurs de vue et d'indiquer au routeur s'il est possible d'intégrer des contrôleurs de vue sur la pile. L'exemple le plus courant d'une telle tâche est l'authentification (mais pas du tout courante dans la mise en œuvre). Par exemple, vous souhaitez afficher un contrôleur de vue avec les détails d'un compte d'utilisateur, mais pour cela, l'utilisateur doit être connecté au système. Vous pouvez implémenter un RoutingInterceptor
et l'ajouter à la configuration de la vue du contrôleur des détails de l'utilisateur et de la vérification interne: si l'utilisateur est connecté, autorisez le routeur à continuer la navigation, sinon, affichez le contrôleur de vue qui invite l'utilisateur à se connecter et si cette action réussit, autorisez le routeur à continuer la navigation ou à annuler elle si l'utilisateur refuse de se connecter.
class LoginInterceptor: RoutingInterceptor { func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) -> Void) { guard !LoginManager.sharedInstance.isUserLoggedIn else {
Une implémentation d'un tel RoutingInterceptor
avec commentaires est contenue dans l'exemple fourni avec la bibliothèque.
ContextTask
L' ContextTask
, si vous la fournissez, peut être appliquée séparément à chaque contrôleur de vue dans la configuration, qu'elle soit juste créée par un routeur ou trouvée sur la pile, et que vous souhaitiez simplement mettre à jour les données et / ou définir des valeurs par défaut paramètres (par exemple, afficher le bouton de fermeture ou ne pas afficher).
PostRoutingTask
L'implémentation de PostRoutingTask
sera appelée par le routeur après la réussite de l'intégration du contrôleur de vue demandé sur la pile. Dans sa mise en œuvre, il est pratique d'ajouter diverses analyses ou de tirer divers services.
Plus en détail avec l'implémentation de toutes les entités décrites peuvent être trouvées dans la documentation de la bibliothèque ainsi que dans l'exemple ci-joint.
PS: Le nombre d'entités auxiliaires pouvant être ajoutées à la configuration n'est pas limité.
La configuration
Toutes les entités décrites sont bonnes en ce sens qu'elles divisent le processus de composition en petits blocs interchangeables et fiables.
Passons maintenant à la chose la plus importante - à la configuration, c'est-à-dire à la connexion de ces blocs entre eux. Afin de collecter ces blocs entre eux et de les combiner en une chaîne d'étapes, la bibliothèque fournit une classe de générateur StepAssembly
(pour les conteneurs - ContainerStepAssembly
). Son implémentation vous permet de chaîner les blocs de composition en un seul objet de configuration comme des perles sur une chaîne, et d'indiquer également les dépendances sur les configurations d'autres contrôleurs de vue. Que faire de la configuration à l'avenir dépend de vous. Vous pouvez le transmettre au routeur avec les paramètres nécessaires et il va construire une pile de contrôleurs pour vous, vous pouvez l'enregistrer dans le dictionnaire et l'utiliser plus tard par clé - cela dépend de votre tâche spécifique.
Prenons un exemple trivial: supposons qu'en cliquant sur une cellule de la liste ou lorsque l'application reçoit un lien universel d'un safari ou d'un client de messagerie, nous devons afficher de manière modale le contrôleur de produit avec un certain ID de produit. Dans ce cas, le contrôleur de produit doit être intégré à l'intérieur de l' UINavigationController
afin qu'il puisse afficher son nom et son bouton de fermeture sur son panneau de commande. De plus, ce produit ne peut être présenté qu'aux utilisateurs connectés, sinon, invitez-les à se connecter.
Si vous analysez cet exemple sans utiliser de bibliothèque, il ressemblera à ceci:
class ProductArrayViewController: UITableViewController { let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance
Cet exemple n'inclut pas la mise en œuvre de liens universels, ce qui nécessitera d'isoler le code d'autorisation et de maintenir le contexte dans lequel l'utilisateur doit être dirigé, ainsi que de rechercher, soudainement, l'utilisateur clique sur un lien, et ce produit lui est déjà montré, ce qui rendra finalement le code très difficile à lire.
Considérez la configuration de cet exemple à l'aide de la bibliothèque:
let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory())
Si vous traduisez cela en langage humain:
- Vérifiez que l'utilisateur est connecté et sinon, offrez-lui une entrée
- Si l'utilisateur s'est connecté avec succès, continuez
- Rechercher le contrôleur d'affichage des produits fourni par
Finder
- S'il a été trouvé - rendre visible et terminer
- S'il n'a pas été trouvé - créez un
UINavigationController
, UINavigationController
-y le contrôleur de vue créé par ProductViewControllerFactory
aide de PushToNavigationAction
GenericActions.PresentModally
UINavigationController
GenericActions.PresentModally
aide de GenericActions.PresentModally
partir du contrôleur de vue actuel
La configuration nécessite une étude, comme de nombreuses solutions complexes, par exemple, le concept d' AutoLayout
et, à première vue, il peut sembler compliqué et redondant. Cependant, le nombre de tâches à résoudre avec le fragment de code donné couvre tous les aspects, de l'autorisation au lien profond, et le fractionnement en une séquence d'actions permet de modifier facilement la configuration sans avoir à apporter de modifications au code. De plus, l'implémentation de StepAssembly
vous aidera à éviter les problèmes avec une chaîne incomplète d'étapes et à contrôler le type - problèmes d'incompatibilité des paramètres d'entrée pour différents contrôleurs de vue.
Considérez le pseudo-code d'une application complète dans laquelle un ProductArrayViewController
affiche une liste de produits et, si l'utilisateur sélectionne ce produit, l'affiche selon que l'utilisateur est connecté ou non, ou propose de se connecter et s'affiche après une connexion réussie:
Objets de configuration
class ProductArrayViewController: UITableViewController { let products: [UUID]?
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
.
. , , , — ProductArrayViewController, UINavigationController HomeViewController — StepAssembly
from()
. RouteComposer
, ( ). , Configuration
. , A/B , .
Au lieu d'une conclusion
, 3 . , , . Fabric
, Finder
Action
. , — , , . , .
, , objective c Cocoa Touch, . iOS 9 12.
UIViewController
(MVC, MVVM, VIP, RIB, VIPER ..)
, , , . . .
.