Organisation der Navigation in iOS-Anwendungen mit dem Root Controller



Die meisten mobilen Anwendungen enthalten mehr als ein Dutzend Bildschirme, komplexe Übergänge sowie Teile der Anwendung, die nach Bedeutung und Zweck getrennt sind. Daher muss die richtige Navigationsstruktur für die Anwendung organisiert werden, die flexibel, bequem und erweiterbar ist, einen bequemen Zugriff auf verschiedene Teile der Anwendung bietet und auch die Systemressourcen berücksichtigt.

In diesem Artikel werden wir die Navigation in der Anwendung so gestalten, dass die häufigsten Fehler vermieden werden, die zu Speicherverlusten führen, die Architektur ruinieren und die Navigationsstruktur beschädigen.

Die meisten Anwendungen bestehen aus mindestens zwei Teilen: Authentifizierung (Pre-Login) und privatem Teil (Post-Login). Einige Anwendungen haben möglicherweise eine komplexere Struktur, mehrere Profile mit einem einzigen Login, bedingte Übergänge nach dem Starten der Anwendung (Deeplinks) usw.

In der Praxis werden hauptsächlich zwei Ansätze zum Navigieren in der Anwendung verwendet:

  1. Ein Navigationsstapel für Präsentationscontroller (vorhanden) und Navigationscontroller (Push), ohne dass Sie zurückkehren können. Dieser Ansatz bewirkt, dass alle vorherigen ViewController im Speicher verbleiben.
  2. Verwendet den Schalter window.rootViewController. Bei diesem Ansatz werden alle vorherigen ViewController im Speicher zerstört, dies sieht jedoch aus Sicht der Benutzeroberfläche nicht optimal aus. Außerdem können Sie sich bei Bedarf nicht hin und her bewegen.

Lassen Sie uns nun sehen, wie Sie eine leicht zu wartende Struktur erstellen können, mit der Sie nahtlos zwischen verschiedenen Teilen der Anwendung wechseln können, ohne Spaghetti-Code und einfache Navigation.

Stellen wir uns vor, wir schreiben eine Anwendung bestehend aus:

  • Primärbildschirm ( Begrüßungsbildschirm ): Dies ist der allererste Bildschirm, den Sie sehen. Sobald die Anwendung gestartet wird, können Sie beispielsweise Animationen hinzufügen oder primäre API-Anforderungen stellen.
  • Authentifizierungsteil : Anmeldung, Registrierung, Zurücksetzen des Passworts, E-Mail-Bestätigung usw. Die Arbeitssitzung des Benutzers wird normalerweise gespeichert, sodass bei jedem Start der Anwendung kein Login eingegeben werden muss.
  • Hauptteil : die Geschäftslogik der Hauptanwendung

Alle diese Teile der Anwendung sind voneinander isoliert und befinden sich in einem eigenen Navigationsstapel. Daher benötigen wir möglicherweise die folgenden Übergänge:

  • Begrüßungsbildschirm -> Authentifizierungsbildschirm , wenn die aktuelle Sitzung des aktiven Benutzers fehlt.
  • Begrüßungsbildschirm -> Hauptbildschirm, wenn sich der Benutzer bereits früher bei der Anwendung angemeldet hat und eine Sitzung aktiv ist.
  • Hauptbildschirm -> Authentifizierungsbildschirm , falls sich der Benutzer abmeldet


Grundeinstellung

Wenn die Anwendung gestartet wird , müssen wir den RootViewController initialisieren, der zuerst geladen wird. Dies kann sowohl mit Code als auch über Interface Builder erfolgen. Erstellen Sie ein neues Projekt in xCode, und all dies wird standardmäßig bereits ausgeführt: main.storyboard ist bereits an window.rootViewController gebunden.

Um uns jedoch auf das Hauptthema des Artikels zu konzentrieren, werden wir in unserem Projekt keine Storyboards verwenden. Löschen Sie daher main.storyboard und deaktivieren Sie das Feld "Hauptschnittstelle" unter Ziele -> Allgemein -> Bereitstellungsinformationen:



Ändern wir nun die didFinishLaunchingWithOptions- Methode in AppDelegate so, dass sie folgendermaßen aussieht:

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

Jetzt startet die Anwendung zuerst den RootViewController . Benennen Sie den Basis- ViewController in RootViewController um :

 class RootViewController: UIViewController { } 

Dies ist der Hauptcontroller, der für alle Übergänge zwischen verschiedenen Abschnitten der Anwendung verantwortlich ist. Daher benötigen wir jedes Mal, wenn wir den Übergang vornehmen möchten, einen Link dazu. Fügen Sie dazu die Erweiterung zu AppDelegate hinzu :

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

Das erzwungene Abrufen einer Option ist in diesem Fall gerechtfertigt, da sich der RootViewController nicht ändert. Wenn dies versehentlich geschieht, ist der Absturz der Anwendung eine normale Situation.

Jetzt haben wir also von überall in der Anwendung einen Link zum RootViewController :

 let rootViewController = AppDelegate.shared.rootViewController 

Jetzt erstellen wir einige weitere Controller, die wir benötigen: SplashViewController, LoginViewController und MainViewController .

Der Begrüßungsbildschirm ist der erste Bildschirm, den ein Benutzer nach dem Starten der Anwendung sieht. Zu diesem Zeitpunkt werden normalerweise alle erforderlichen API-Anforderungen gestellt, die Aktivität der Benutzersitzung wird überprüft usw. Verwenden Sie die UIActivityIndicatorView, um die laufenden Hintergrundaktionen anzuzeigen :

 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() { } } 

Um API-Anforderungen zu simulieren, fügen Sie die Methode DispatchQueue.main.asyncAfter mit einer Verzögerung von 3 Sekunden hinzu:

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

Wir glauben, dass die Benutzersitzung auch in diesen Anforderungen festgelegt ist. In unserer Anwendung verwenden wir UserDefaults dafür:

 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 } } } 

Sie werden UserDefaults definitiv nicht verwenden, um den Status einer Benutzersitzung in der Release-Version des Programms zu speichern. Wir verwenden lokale Einstellungen in unserem Projekt, um das Verständnis zu vereinfachen und nicht viel über das Hauptthema des Artikels hinauszugehen.

Erstellen Sie einen LoginViewController . Es wird verwendet, um den Benutzer zu authentifizieren, wenn die aktuelle Benutzersitzung inaktiv ist. Sie können Ihre benutzerdefinierte Benutzeroberfläche zum Controller hinzufügen, aber ich werde hier nur den Bildschirmtitel und die Anmeldeschaltfläche in der Navigationsleiste hinzufügen.

 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 } } 

Erstellen Sie abschließend den Hauptcontroller der MainViewController- Anwendung:

 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 } } 

Root-Navigation

Nun zurück zum RootViewController .
Wie bereits erwähnt, ist RootViewController das einzige Objekt, das für Übergänge zwischen verschiedenen unabhängigen Controller-Stacks verantwortlich ist. Um den aktuellen Status der Anwendung zu kennen, erstellen wir eine Variable, in der wir den aktuellen ViewController speichern:

 class RootViewController: UIViewController { private var current: UIViewController } 

Fügen Sie den Klasseninitialisierer hinzu und erstellen Sie den ersten ViewController , den wir beim Start der Anwendung laden möchten. In unserem Fall ist es SplashViewController :

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

Fügen Sie in viewDidLoad den aktuellen viewController zum RootViewController hinzu :

 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 } } 

Sobald wir childViewController (1) hinzugefügt haben , passen wir seine Größe an, indem wir current.view.frame auf view.bounds (2) setzen.

Wenn wir diese Zeile überspringen, wird der viewController in den meisten Fällen immer noch korrekt platziert. Bei Änderungen der Frame- Größe können jedoch Probleme auftreten.

Fügen Sie eine neue Unteransicht (3) hinzu und rufen Sie die Methode didMove (toParentViewController :) auf. Damit ist der Vorgang zum Hinzufügen des Controllers abgeschlossen. Sobald der RootViewController hochfährt , wird unmittelbar danach der SplashViewController angezeigt.

Jetzt können Sie der Anwendung verschiedene Navigationsmethoden hinzufügen. Wir werden den LoginViewController ohne Animation anzeigen, der MainViewController wird die Animation mit sanftem Dimmen verwenden und der Übergang der Bildschirme beim Trennen des Benutzers hat einen Folieneffekt.

 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 } 

Erstellen Sie den LoginViewController (1), fügen Sie ihn als untergeordneten Controller hinzu (2) und setzen Sie den Frame (3). Fügen Sie die Ansicht von LoginController als Unteransicht (4) hinzu und rufen Sie die Methode didMove (5) auf. Bereiten Sie als Nächstes den aktuellen Controller für das Entfernen mit der Methode willMove (6) vor. Löschen Sie abschließend die aktuelle Ansicht aus der Übersicht (7) und den aktuellen Controller aus RootViewController (8). Denken Sie daran, den Wert des aktuellen Reglers (9) zu aktualisieren.

Erstellen wir nun die switchToMainScreen- Methode:

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

Das Animieren des Übergangs erfordert eine andere Methode:

 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 } } 

Diese Methode ist showLoginScreen sehr ähnlich, aber alle letzten Schritte werden ausgeführt, nachdem die Animation abgeschlossen ist. Um die aufrufende Methode über das Ende des Übergangs zu informieren, rufen wir ganz am Ende den Abschluss (1) auf.

Die endgültige Version der switchToMainScreen- Methode sieht nun folgendermaßen aus:

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

Und schließlich erstellen wir die letzte Methode, die für den Übergang von MainViewController zu LoginViewController verantwortlich ist :

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

Die AnimateDismissTransition- Methode bietet Folienanimation :

 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?() } } 

Dies sind nur zwei Beispiele für Animationen. Mit demselben Ansatz können Sie alle komplexen Animationen erstellen, die Sie benötigen

Fügen Sie zum Abschließen der Konfiguration Methodenaufrufe mit Animationen von SplashViewController, LoginViewController und MainViewController hinzu :

 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() } } 

Kompilieren Sie die Anwendung, führen Sie sie aus und überprüfen Sie ihren Betrieb auf zwei Arten:

- wenn der Benutzer bereits eine aktive aktuelle Sitzung hat (angemeldet)
- wenn keine aktive Sitzung vorhanden ist und eine Authentifizierung erforderlich ist

In beiden Fällen sollte unmittelbar nach dem Laden von SplashScreen ein Übergang zum gewünschten Bildschirm angezeigt werden.



Als Ergebnis haben wir ein kleines Testmodell der Anwendung erstellt, mit Navigation durch die Hauptmodule. Wenn Sie die Funktionen der Anwendung erweitern, zusätzliche Module und Übergänge zwischen ihnen hinzufügen müssen, können Sie dieses Navigationssystem jederzeit schnell und bequem erweitern und skalieren.

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


All Articles