Organisation de la navigation dans les applications iOS à l'aide du Root Controller



La plupart des applications mobiles contiennent plus d'une douzaine d'écrans, des transitions complexes, ainsi que des parties de l'application, séparées par leur signification et leur objectif. Par conséquent, il est nécessaire d'organiser la structure de navigation correcte pour l'application, qui sera flexible, pratique, extensible, fournira un accès confortable aux différentes parties de l'application et fera également attention aux ressources du système.

Dans cet article, nous allons concevoir la navigation dans l'application de manière à éviter les erreurs les plus courantes qui entraînent des fuites de mémoire, ruinent l'architecture et cassent la structure de navigation.

La plupart des applications comportent au moins deux parties: l'authentification (pré-connexion) et la partie privée (post-connexion). Certaines applications peuvent avoir une structure plus complexe, plusieurs profils avec une seule connexion, des transitions conditionnelles après le démarrage de l'application (liens profonds), etc.

En pratique, deux approches sont principalement utilisées pour naviguer dans l'application:

  1. Une pile de navigation pour les contrôleurs de présentation (présents) et les contrôleurs de navigation (push), sans possibilité de revenir en arrière. Cette approche fait que tous les ViewControllers précédents restent en mémoire.
  2. Utilise la bascule window.rootViewController. Avec cette approche, tous les ViewControllers précédents sont détruits en mémoire, mais cela ne semble pas le meilleur du point de vue de l'interface utilisateur. De plus, il ne vous permet pas de vous déplacer d'avant en arrière si nécessaire.

Voyons maintenant comment vous pouvez créer une structure facilement maintenable qui vous permet de basculer de manière transparente entre les différentes parties de l'application, sans code spaghetti et navigation facile.

Imaginons que nous écrivons une application composée de:

  • L'écran principal (écran Splash ): c'est le tout premier écran que vous voyez, dès que l'application démarre, vous pouvez ajouter, par exemple, une animation ou faire des requêtes API principales.
  • Partie authentification : connexion, enregistrement, réinitialisation du mot de passe, confirmation par e-mail, etc. La session de travail de l'utilisateur est généralement enregistrée, il n'est donc pas nécessaire d'entrer une connexion à chaque démarrage de l'application.
  • Partie principale : la logique métier de l'application principale

Toutes ces parties de l'application sont isolées les unes des autres et existent chacune dans sa propre pile de navigation. Par conséquent, nous pouvons avoir besoin des transitions suivantes:

  • Écran de démarrage -> écran d'authentification , si la session en cours de l'utilisateur actif est absente.
  • Écran de démarrage -> Écran principal , si l'utilisateur s'est déjà connecté à l'application plus tôt et qu'il y a une session active.
  • Écran principal -> écran d'authentification , au cas où l'utilisateur se déconnecterait


Réglage de base

Lorsque l'application démarre, nous devons initialiser le RootViewController , qui sera chargé en premier. Cela peut être fait à la fois avec du code et via Interface Builder. Créez un nouveau projet dans xCode et tout cela sera déjà fait par défaut: main.storyboard est déjà lié à window.rootViewController .

Mais afin de nous concentrer sur le sujet principal de l'article, nous n'utiliserons pas de storyboards dans notre projet. Par conséquent, supprimez main.storyboard et effacez également le champ «Interface principale» sous Cibles -> Général -> Informations de déploiement:



Modifions maintenant la méthode didFinishLaunchingWithOptions dans AppDelegate pour qu'elle ressemble à ceci:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = RootViewController() window?.makeKeyAndVisible() return true } 

Maintenant, l'application lancera d'abord le RootViewController . Renommez le ViewController de base en RootViewController :

 class RootViewController: UIViewController { } 

Ce sera le contrôleur principal responsable de toutes les transitions entre les différentes sections de l'application. Par conséquent, nous aurons besoin d'un lien vers celui-ci chaque fois que nous voulons effectuer la transition. Pour ce faire, ajoutez l'extension à AppDelegate :

 extension AppDelegate { static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate } var rootViewController: RootViewController { return window!.rootViewController as! RootViewController } } 

La récupération forcée d'une option dans ce cas est justifiée, car le RootViewController ne change pas, et si cela se produit par accident, le plantage de l'application est une situation normale.

Donc, maintenant nous avons un lien vers le RootViewController de n'importe où dans l'application:

 let rootViewController = AppDelegate.shared.rootViewController 

Créons maintenant quelques contrôleurs supplémentaires dont nous avons besoin: SplashViewController, LoginViewController et MainViewController .

Splash Screen est le premier écran qu'un utilisateur verra après le démarrage de l'application. À ce stade, toutes les demandes d'API nécessaires sont généralement effectuées, l'activité de session utilisateur est vérifiée, etc. Pour afficher les actions d'arrière-plan en cours, utilisez UIActivityIndicatorView :

 class SplashViewController: UIViewController { private let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge) override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.white view.addSubview(activityIndicator) activityIndicator.frame = view.bounds activityIndicator.backgroundColor = UIColor(white: 0, alpha: 0.4) makeServiceCall() } private func makeServiceCall() { } } 

Afin de simuler des demandes d'API, ajoutez la méthode DispatchQueue.main.asyncAfter avec un délai de 3 secondes:

 private func makeServiceCall() { activityIndicator.startAnimating() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) { self.activityIndicator.stopAnimating() } } 

Nous pensons que la session utilisateur est également définie dans ces demandes. Dans notre application, nous utilisons pour cela UserDefaults :

 private func makeServiceCall() { activityIndicator.startAnimating() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) { self.activityIndicator.stopAnimating() if UserDefaults.standard.bool(forKey: “LOGGED_IN”) { // navigate to protected page } else { // navigate to login screen } } } 

Vous n'utiliserez certainement pas UserDefaults pour enregistrer l'état d'une session utilisateur dans la version finale du programme. Nous utilisons des paramètres locaux dans notre projet pour simplifier la compréhension et ne pas aller bien au-delà du sujet principal de l'article.

Créez un LoginViewController . Il sera utilisé pour authentifier l'utilisateur si la session utilisateur actuelle est inactive. Vous pouvez ajouter votre interface utilisateur personnalisée au contrôleur, mais je n'ajouterai ici que le titre de l'écran et le bouton de connexion dans la barre de navigation.

 class LoginViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.white title = "Login Screen" let loginButton = UIBarButtonItem(title: "Log In", style: .plain, target: self, action: #selector(login)) navigationItem.setLeftBarButton(loginButton, animated: true) } @objc private func login() { // store the user session (example only, not for the production) UserDefaults.standard.set(true, forKey: "LOGGED_IN") // navigate to the Main Screen } } 

Et enfin, créez le contrôleur principal de l'application MainViewController :

 class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.lightGray // to visually distinguish the protected part title = “Main Screen” let logoutButton = UIBarButtonItem(title: “Log Out”, style: .plain, target: self, action: #selector(logout)) navigationItem.setLeftBarButton(logoutButton, animated: true) } @objc private func logout() { // clear the user session (example only, not for the production) UserDefaults.standard.set(false, forKey: “LOGGED_IN”) // navigate to the Main Screen } } 

Navigation racine

Revenons maintenant au RootViewController .
Comme nous l'avons dit précédemment, RootViewController est le seul objet responsable des transitions entre les différentes piles de contrôleurs indépendants. Afin de connaître l'état actuel de l'application, nous allons créer une variable dans laquelle nous allons stocker le ViewController actuel:

 class RootViewController: UIViewController { private var current: UIViewController } 

Ajoutez l'initialiseur de classe et créez le premier ViewController que nous voulons charger au démarrage de l'application. Dans notre cas, ce sera SplashViewController :

 class RootViewController: UIViewController { private var current: UIViewController init() { self.current = SplashViewController() super.init(nibName: nil, bundle: nil) } } 

Dans viewDidLoad, ajoutez le viewController actuel au RootViewController :

 class RootViewController: UIViewController { ... override func viewDidLoad() { super.viewDidLoad() addChildViewController(current) // 1 current.view.frame = view.bounds // 2 view.addSubview(current.view) // 3 current.didMove(toParentViewController: self) // 4 } } 

Une fois que nous avons ajouté childViewController (1), nous ajustons sa taille en définissant current.view.frame sur view.bounds (2).

Si nous sautons cette ligne, le viewController sera toujours placé correctement dans la plupart des cas, mais des problèmes peuvent survenir si la taille du cadre change.

Ajoutez une nouvelle sous-vue (3) et appelez la méthode didMove (toParentViewController :). Ceci terminera l'opération d'ajout de contrôleur. Dès que le RootViewController démarre , immédiatement après que le SplashViewController est affiché.

Vous pouvez maintenant ajouter plusieurs méthodes de navigation dans l'application. Nous afficherons le LoginViewController sans aucune animation, le MainViewController utilisera l'animation avec une gradation douce et la transition des écrans lors de la déconnexion de l'utilisateur aura un effet de diapositive.

 class RootViewController: UIViewController { ... func showLoginScreen() { let new = UINavigationController(rootViewController: LoginViewController()) // 1 addChildViewController(new) // 2 new.view.frame = view.bounds // 3 view.addSubview(new.view) // 4 new.didMove(toParentViewController: self) // 5 current.willMove(toParentViewController: nil) // 6 current.view.removeFromSuperview()] // 7 current.removeFromParentViewController() // 8 current = new // 9 } 

Créez LoginViewController (1), ajoutez comme contrôleur enfant (2), définissez le cadre (3). Ajoutez la vue de LoginController en tant que sous-vue (4) et appelez la méthode didMove (5). Ensuite, préparez le contrôleur actuel pour le retrait à l'aide de la méthode willMove (6). Enfin, supprimez la vue actuelle de superview (7) et supprimez le contrôleur actuel de RootViewController (8). N'oubliez pas de mettre à jour la valeur du contrôleur actuel (9).

Créons maintenant la méthode switchToMainScreen :

 func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = UINavigationController(rootViewController: mainViewController) ... } 

L'animation de la transition nécessite une méthode différente:

 private func animateFadeTransition(to new: UIViewController, completion: (() -> Void)? = nil) { current.willMove(toParentViewController: nil) addChildViewController(new) transition(from: current, to: new, duration: 0.3, options: [.transitionCrossDissolve, .curveEaseOut], animations: { }) { completed in self.current.removeFromParentViewController() new.didMove(toParentViewController: self) self.current = new completion?() //1 } } 

Cette méthode est très similaire à showLoginScreen , mais toutes les dernières étapes sont effectuées une fois l'animation terminée. Afin de notifier la méthode d'appel de la fin de la transition, à la toute fin nous appelons la fermeture (1).

Maintenant, la version finale de la méthode switchToMainScreen ressemblera à ceci:

 func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = UINavigationController(rootViewController: mainViewController) animateFadeTransition(to: mainScreen) } 

Et enfin, créons la dernière méthode qui sera responsable de la transition de MainViewController à LoginViewController :

 func switchToLogout() { let loginViewController = LoginViewController() let logoutScreen = UINavigationController(rootViewController: loginViewController) animateDismissTransition(to: logoutScreen) } 

La méthode AnimateDismissTransition fournit une animation de diapositive:

 private func animateDismissTransition(to new: UIViewController, completion: (() -> Void)? = nil) { new.view.frame = CGRect(x: -view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height) current.willMove(toParentViewController: nil) addChildViewController(new) transition(from: current, to: new, duration: 0.3, options: [], animations: { new.view.frame = self.view.bounds }) { completed in self.current.removeFromParentViewController() new.didMove(toParentViewController: self) self.current = new completion?() } } 

Ce ne sont que deux exemples d'animation, en utilisant la même approche, vous pouvez créer toutes les animations complexes dont vous avez besoin

Pour terminer la configuration, ajoutez des appels de méthode avec des animations de SplashViewController, LoginViewController et MainViewController :

 class SplashViewController: UIViewController { ... private func makeServiceCall() { if UserDefaults.standard.bool(forKey: “LOGGED_IN”) { // navigate to protected page AppDelegate.shared.rootViewController.switchToMainScreen() } else { // navigate to login screen AppDelegate.shared.rootViewController.switchToLogout() } } } class LoginViewController: UIViewController { ... @objc private func login() { ... AppDelegate.shared.rootViewController.switchToMainScreen() } } class MainViewController: UIViewController { ... @objc private func logout() { ... AppDelegate.shared.rootViewController.switchToLogout() } } 

Compilez, exécutez l'application et vérifiez son fonctionnement de deux manières:

- lorsque l'utilisateur a déjà une session active active (connecté)
- lorsqu'il n'y a pas de session active et qu'une authentification est requise

Dans les deux cas, vous devriez voir une transition vers l'écran souhaité, immédiatement après le chargement de SplashScreen .



En conséquence, nous avons créé un petit modèle de test de l'application, avec navigation à travers ses principaux modules. Si vous avez besoin d'étendre les capacités de l'application, d'ajouter des modules supplémentaires et des transitions entre eux, vous pouvez toujours étendre et mettre à l'échelle ce système de navigation de manière rapide et pratique.

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


All Articles