
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:
- 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.
- 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 baseLorsque 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”) {
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() {
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 racineRevenons 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)
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?()
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.