Organização da navegação em aplicativos iOS usando o Controlador Raiz



A maioria dos aplicativos móveis contém mais de uma dúzia de telas, transições complexas e partes do aplicativo, separadas por significado e finalidade. Portanto, é necessário organizar a estrutura de navegação correta para o aplicativo, que será flexível, conveniente, expansível, proporcionará acesso confortável a várias partes do aplicativo e também será cuidadoso com os recursos do sistema.

Neste artigo, projetaremos a navegação no aplicativo de forma a evitar os erros mais comuns que causam vazamentos de memória, arruinam a arquitetura e quebram a estrutura de navegação.

A maioria dos aplicativos possui pelo menos duas partes: autenticação (pré-login) e parte privada (pós-login). Alguns aplicativos podem ter uma estrutura mais complexa, vários perfis com um único login, transições condicionais após o início do aplicativo (deeplinks) etc.

Na prática, duas abordagens são usadas principalmente para navegar no aplicativo:

  1. Uma pilha de navegação para os controladores de apresentação (presente) e controladores de navegação (push), sem a capacidade de voltar. Essa abordagem faz com que todos os ViewControllers anteriores permaneçam na memória.
  2. Usa alternar window.rootViewController. Com essa abordagem, todos os ViewControllers anteriores são destruídos na memória, mas isso não parece o melhor do ponto de vista da interface do usuário. Além disso, ele não permite que você se mova para frente e para trás, se necessário.

Agora vamos ver como você pode criar uma estrutura de fácil manutenção que permite alternar facilmente entre diferentes partes do aplicativo, sem código de espaguete e fácil navegação.

Vamos imaginar que estamos escrevendo um aplicativo que consiste em:

  • A tela principal ( tela inicial ): esta é a primeira tela exibida, assim que o aplicativo é iniciado, você pode adicionar, por exemplo, animação ou fazer solicitações de API primárias.
  • Parte de autenticação : login, registro, redefinição de senha, confirmação por email, etc. A sessão de trabalho do usuário geralmente é salva, portanto, não é necessário inserir um logon sempre que o aplicativo for iniciado.
  • Parte principal : a lógica de negócios do aplicativo principal

Todas essas partes do aplicativo são isoladas uma da outra e cada uma existe em sua própria pilha de navegação. Portanto, podemos precisar das seguintes transições:

  • Tela inicial -> Tela de autenticação , se a sessão atual do usuário ativo estiver ausente.
  • Tela inicial -> Tela principal , se o usuário já tiver efetuado login no aplicativo anteriormente e houver uma sessão ativa.
  • Tela principal -> Tela de autenticação , caso o usuário efetue logout


Configuração básica

Quando o aplicativo é iniciado, precisamos inicializar o RootViewController , que será carregado primeiro. Isso pode ser feito com o código e através do Interface Builder. Crie um novo projeto no xCode e tudo isso já será feito por padrão: main.storyboardestá vinculado a window.rootViewController .

Mas, para focar no tópico principal do artigo, não usaremos storyboards em nosso projeto. Portanto, exclua main.storyboard e também limpe o campo "Interface principal" em Destinos -> Geral -> Informações de implantação:



Agora vamos alterar o método didFinishLaunchingWithOptions no AppDelegate para que fique assim:

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

Agora, o aplicativo iniciará primeiro o RootViewController . Renomeie o ViewController base para o RootViewController :

 class RootViewController: UIViewController { } 

Este será o principal controlador responsável por todas as transições entre diferentes seções do aplicativo. Portanto, precisaremos de um link para ele toda vez que quisermos fazer a transição. Para fazer isso, adicione a extensão ao AppDelegate :

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

A recuperação forçada de uma opção nesse caso é justificada, porque o RootViewController não muda e, se isso acontecer repentinamente por acidente, a falha do aplicativo é uma situação normal.

Portanto, agora temos um link para o RootViewController de qualquer lugar do aplicativo:

 let rootViewController = AppDelegate.shared.rootViewController 

Agora vamos criar mais alguns controladores que precisamos: SplashViewController, LoginViewController e MainViewController .

Tela inicial é a primeira tela que um usuário verá após iniciar o aplicativo. No momento, todas as solicitações de API necessárias geralmente são feitas, a atividade da sessão do usuário é verificada etc. Para exibir as ações em segundo plano em andamento, use o 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 solicitações de API, adicione o método DispatchQueue.main.asyncAfter com um atraso de 3 segundos:

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

Acreditamos que a sessão do usuário também esteja definida nessas solicitações. Em nossa aplicação, usamos UserDefaults para isso:

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

Você definitivamente não usará UserDefaults para salvar o estado de uma sessão de usuário na versão do programa. Usamos configurações locais em nosso projeto para simplificar a compreensão e não ir muito além do tópico principal do artigo.

Crie um LoginViewController . Será usado para autenticar o usuário se a sessão atual do usuário estiver inativa. Você pode adicionar sua interface de usuário personalizada ao controlador, mas adicionarei aqui apenas o título da tela e o botão de login na barra de navegação.

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

E, finalmente, crie o controlador principal do aplicativo 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 } } 

Navegação raiz

Agora, de volta ao RootViewController .
Como dissemos anteriormente, o RootViewController é o único objeto responsável pelas transições entre diferentes pilhas de controladores independentes. Para estar ciente do estado atual do aplicativo, criaremos uma variável na qual armazenaremos o ViewController atual:

 class RootViewController: UIViewController { private var current: UIViewController } 

Adicione o inicializador de classe e crie o primeiro ViewController que queremos carregar quando o aplicativo for iniciado. No nosso caso, será o SplashViewController :

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

Em viewDidLoad, adicione o viewController atual ao 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 } } 

Depois de adicionar childViewController (1), ajustamos seu tamanho configurando current.view.frame para view.bounds (2).

Se pularmos essa linha, o viewController ainda será colocado corretamente na maioria dos casos, mas poderão ocorrer problemas se o tamanho do quadro for alterado.

Adicione uma nova subview (3) e chame o método didMove (toParentViewController :). Isso concluirá a operação de adição do controlador. Assim que o RootViewController inicializar , imediatamente depois disso, o SplashViewController será exibido.

Agora você pode adicionar vários métodos de navegação no aplicativo. Exibiremos o LoginViewController sem nenhuma animação, o MainViewController usará a animação com escurecimento suave, e a transição das telas ao desconectar o usuário terá um efeito de slide.

 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 } 

Crie o LoginViewController (1), adicione como um controlador filho (2), defina o quadro (3). Adicione a visualização do LoginController como subview (4) e chame o método didMove (5). Em seguida, prepare o controlador atual para remoção usando o método willMove (6). Por fim, exclua a visualização atual da superview (7) e exclua o controlador atual do RootViewController (8). Lembre-se de atualizar o valor do controlador atual (9).

Agora vamos criar o método switchToMainScreen :

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

Animar a transição requer um 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 } } 

Esse método é muito semelhante ao showLoginScreen , mas todas as últimas etapas são executadas após a conclusão da animação. Para notificar o método de chamada do final da transição, no final chamamos de encerramento (1).

Agora a versão final do método switchToMainScreen será parecida com esta:

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

E, finalmente, vamos criar o último método que será responsável pela transição do MainViewController para o LoginViewController :

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

O método AnimateDismissTransition fornece animação de slide:

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

Estes são apenas dois exemplos de animação. Usando a mesma abordagem, você pode criar animações complexas necessárias.

Para concluir a configuração, adicione chamadas de método com animações do SplashViewController, LoginViewController e 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, execute o aplicativo e verifique sua operação de duas maneiras:

- quando o usuário já tiver uma sessão atual ativa (logado)
- quando não houver sessão ativa e a autenticação for necessária

Nos dois casos, você verá uma transição para a tela desejada, imediatamente após carregar o SplashScreen .



Como resultado, criamos um pequeno modelo de teste do aplicativo, com navegação por seus principais módulos. Se você precisar expandir os recursos do aplicativo, adicionar módulos e transições adicionais entre eles, sempre poderá expandir e escalar com rapidez e conveniência esse sistema de navegação.

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


All Articles