Organización de la navegación en aplicaciones iOS usando el Root Controller



La mayoría de las aplicaciones móviles contienen más de una docena de pantallas, transiciones complejas, así como partes de la aplicación, separadas por significado y propósito. Por lo tanto, existe la necesidad de organizar la estructura de navegación correcta para la aplicación, que será flexible, conveniente, ampliable, proporcionará un acceso cómodo a varias partes de la aplicación y también tendrá cuidado con los recursos del sistema.

En este artículo, diseñaremos la navegación en la aplicación para evitar los errores más comunes que provocan pérdidas de memoria, arruinan la arquitectura y rompen la estructura de navegación.

La mayoría de las aplicaciones tienen al menos dos partes: autenticación (pre-inicio de sesión) y parte privada (post-inicio de sesión). Algunas aplicaciones pueden tener una estructura más compleja, múltiples perfiles con un solo inicio de sesión, transiciones condicionales después de iniciar la aplicación (enlaces profundos), etc.

En la práctica, dos enfoques se utilizan principalmente para navegar por la aplicación:

  1. Una pila de navegación para los controladores de presentación (presente) y los controladores de navegación (push), sin la capacidad de retroceder. Este enfoque hace que todos los ViewControllers anteriores permanezcan en la memoria.
  2. Utiliza la alternancia window.rootViewController. Con este enfoque, todos los ViewControllers anteriores se destruyen en la memoria, pero esto no se ve mejor desde el punto de vista de la IU. Además, no le permite moverse de un lado a otro si es necesario.

Ahora veamos cómo puede crear una estructura fácil de mantener que le permita cambiar sin problemas entre diferentes partes de la aplicación, sin código de espagueti y fácil navegación.

Imaginemos que estamos escribiendo una aplicación que consiste en:

  • La pantalla principal (pantalla de bienvenida ): esta es la primera pantalla que ve, tan pronto como se inicia la aplicación, puede agregar, por ejemplo, animación o realizar cualquier solicitud de API principal.
  • Parte de autenticación : inicio de sesión, registro, restablecimiento de contraseña, confirmación por correo electrónico, etc. La sesión de trabajo del usuario generalmente se guarda, por lo que no es necesario ingresar cada vez que se inicia la aplicación.
  • Parte principal : la lógica empresarial de la aplicación principal

Todas estas partes de la aplicación están aisladas unas de otras y cada una existe en su propia pila de navegación. Por lo tanto, podemos necesitar las siguientes transiciones:

  • Pantalla de bienvenida -> Pantalla de autenticación , si la sesión actual del usuario activo está ausente.
  • Pantalla de bienvenida -> Pantalla principal , si el usuario ya ha iniciado sesión en la aplicación anteriormente y hay una sesión activa.
  • Pantalla principal -> Pantalla de autenticación , en caso de que el usuario cierre sesión


Ajuste básico

Cuando se inicia la aplicación, necesitamos inicializar el RootViewController , que se cargará primero. Esto se puede hacer tanto con código como a través de Interface Builder. Cree un nuevo proyecto en xCode y todo esto ya se hará de forma predeterminada: main.storyboard ya está vinculado a window.rootViewController .

Pero para centrarnos en el tema principal del artículo, no utilizaremos guiones gráficos en nuestro proyecto. Por lo tanto, elimine main.storyboard y también borre el campo "Interfaz principal" en Objetivos -> General -> Información de implementación:



Ahora cambiemos el método didFinishLaunchingWithOptions en AppDelegate para que se vea así:

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

Ahora la aplicación iniciará primero el RootViewController . Cambie el nombre del ViewController base al RootViewController :

 class RootViewController: UIViewController { } 

Este será el controlador principal responsable de todas las transiciones entre diferentes secciones de la aplicación. Por lo tanto, necesitaremos un enlace cada vez que queramos hacer la transición. Para hacer esto, agregue la extensión a AppDelegate :

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

La recuperación forzada de una opción en este caso está justificada, porque RootViewController no cambia, y si esto ocurre por accidente, el bloqueo de la aplicación es una situación normal.

Entonces, ahora tenemos un enlace al RootViewController desde cualquier lugar de la aplicación:

 let rootViewController = AppDelegate.shared.rootViewController 

Ahora creemos algunos controladores más que necesitamos: SplashViewController, LoginViewController y MainViewController .

La pantalla de bienvenida es la primera pantalla que verá un usuario después de iniciar la aplicación. En este momento, generalmente se realizan todas las solicitudes API necesarias, se verifica la actividad de la sesión del usuario, etc. Para mostrar las acciones en segundo plano en curso, use 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() { } } 

Para simular solicitudes de API, agregue el método DispatchQueue.main.asyncAfter con un retraso de 3 segundos:

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

Creemos que la sesión del usuario también se establece en estas solicitudes. En nuestra aplicación, usamos UserDefaults para esto:

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

Definitivamente no utilizará UserDefaults para guardar el estado de una sesión de usuario en la versión de lanzamiento del programa. Utilizamos configuraciones locales en nuestro proyecto para simplificar la comprensión y no ir mucho más allá del tema principal del artículo.

Cree un LoginViewController . Se utilizará para autenticar al usuario si la sesión de usuario actual está inactiva. Puede agregar su IU personalizada al controlador, pero agregaré aquí solo el título de la pantalla y el botón de inicio de sesión en la barra de navegación.

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

Y finalmente, cree el controlador principal de la aplicación 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 } } 

Navegación de raíz

Ahora volvamos al RootViewController .
Como dijimos anteriormente, RootViewController es el único objeto responsable de las transiciones entre diferentes pilas de controladores independientes. Para conocer el estado actual de la aplicación, crearemos una variable en la que almacenaremos el ViewController actual:

 class RootViewController: UIViewController { private var current: UIViewController } 

Agregue el inicializador de clase y cree el primer ViewController que queremos cargar cuando se inicia la aplicación. En nuestro caso, será SplashViewController :

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

En viewDidLoad, agregue el viewController actual al 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 } } 

Una vez que agregamos childViewController (1), ajustamos su tamaño estableciendo current.view.frame en view.bounds (2).

Si omitimos esta línea, viewController todavía se colocará correctamente en la mayoría de los casos, pero pueden surgir problemas si cambia el tamaño del marco .

Agregue una nueva subvista (3) y llame al método didMove (toParentViewController :). Esto completará la operación de agregar controlador. Tan pronto como se inicia RootViewController , inmediatamente después se muestra SplashViewController .

Ahora puede agregar varios métodos de navegación en la aplicación. Mostraremos el LoginViewController sin ninguna animación, MainViewController usará la animación con atenuación suave y la transición de las pantallas al desconectar al usuario tendrá un efecto deslizante.

 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 } 

Cree LoginViewController (1), agregue como controlador secundario (2), establezca el marco (3). Agregue la vista de LoginController como subview (4) y llame al método didMove (5). A continuación, prepare el controlador actual para su eliminación utilizando el método willMove (6). Finalmente, elimine la vista actual de la supervista (7) y elimine el controlador actual de RootViewController (8). Recuerde actualizar el valor del controlador actual (9).

Ahora creemos el método switchToMainScreen :

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

Animar la transición requiere un método diferente:

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

Este método es muy similar a showLoginScreen , pero todos los últimos pasos se realizan una vez que se completa la animación. Para notificar al método de llamada el final de la transición, al final llamamos cierre (1).

Ahora la versión final del método switchToMainScreen se verá así:

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

Y finalmente, creemos el último método que será responsable de la transición de MainViewController a LoginViewController :

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

El método AnimateDismissTransition proporciona animación de diapositivas:

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

Estos son solo dos ejemplos de animación, con el mismo enfoque puede crear cualquier animación compleja que necesite

Para completar la configuración, agregue llamadas a métodos con animaciones de SplashViewController, LoginViewController y 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() } } 

Compile, ejecute la aplicación y verifique su funcionamiento de dos maneras:

- cuando el usuario ya tiene una sesión activa actual (iniciada sesión)
- cuando no hay sesión activa y se requiere autenticación

En ambos casos, debería ver una transición a la pantalla deseada, inmediatamente después de cargar SplashScreen .



Como resultado, creamos un pequeño modelo de prueba de la aplicación, con navegación a través de sus módulos principales. Si necesita expandir las capacidades de la aplicación, agregar módulos adicionales y transiciones entre ellos, siempre puede expandir y escalar este sistema de navegación de manera rápida y conveniente.

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


All Articles