
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:
- 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.
- 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ásicoCuando 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”) {
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() {
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ízAhora 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)
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?()
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.