ios - Práticas recomendadas para a tela de login do Storyboard, lidando com a limpeza de dados no logout




uistoryboard clear (9)

Estou construindo um aplicativo para iOS usando um Storyboard. O controlador de visualização raiz é um Controlador da Barra de Guias. Estou criando o processo de login / logout, e está funcionando normalmente, mas tenho alguns problemas. Eu preciso saber a melhor maneira de definir tudo isso.

Eu quero realizar o seguinte:

  1. Mostrar uma tela de login na primeira vez que o aplicativo for iniciado. Quando eles fizerem login, vá para a primeira guia do Controlador da Barra de Guias.
  2. Sempre que eles iniciarem o aplicativo depois disso, verifique se eles estão conectados e pule diretamente para a primeira guia do Controlador da barra de guias raiz.
  3. Quando clicarem manualmente em um botão de logout, mostre a tela de login e limpe todos os dados dos controladores de visualização.

O que eu fiz até agora foi definir o controlador de visualização de raiz para o Controlador de Barra de Guias e criei um acompanhamento personalizado para o meu controlador de visualização de Login. Dentro da minha classe Tab Bar Controller, eu verifico se eles estão logados dentro do método viewDidAppear , e execute o seguinte: [self performSegueWithIdentifier:@"pushLogin" sender:self];

Também configurei uma notificação para quando a ação de logout precisa ser executada: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(logoutAccount) name:@"logoutAccount" object:nil];

Após o logout, eu limpo as credenciais do Keychain, execute [self setSelectedIndex:0] e execute o segue para mostrar o controlador de visualização de login novamente.

Isso tudo funciona bem, mas eu estou querendo saber: esta lógica deve estar no AppDelegate? Eu também tenho dois problemas:

  • A primeira vez que eles iniciam o aplicativo , o Controlador da Barra de Guias mostra brevemente antes que a execução seja executada. Eu tentei mover o código para viewWillAppear mas o segue não funcionará tão cedo.
  • Quando eles fazem logout, todos os dados ainda estão dentro de todos os controladores de visualização. Se eles fizerem login em uma nova conta, os dados da conta antiga ainda serão exibidos até que sejam atualizados. Eu preciso de uma maneira de limpar isso facilmente no logout.

Estou aberto para reformular isso. Eu considerei fazer a tela de login o controlador de visão raiz, ou criar um controlador de navegação no AppDelegate para lidar com tudo ... Eu não tenho certeza qual é o melhor método neste momento.


Aqui está minha solução Swifty para qualquer espectador futuro.

1) Crie um protocolo para lidar com as funções de login e logout:

protocol LoginFlowHandler {
    func handleLogin(withWindow window: UIWindow?)
    func handleLogout(withWindow window: UIWindow?)
}

2) Estenda esse protocolo e forneça a funcionalidade aqui para efetuar logout:

extension LoginFlowHandler {

    func handleLogin(withWindow window: UIWindow?) {

        if let _ = AppState.shared.currentUserId {
            //User has logged in before, cache and continue
            self.showMainApp(withWindow: window)
        } else {
            //No user information, show login flow
            self.showLogin(withWindow: window)
        }
    }

    func handleLogout(withWindow window: UIWindow?) {

        AppState.shared.signOut()

        showLogin(withWindow: window)
    }

    func showLogin(withWindow window: UIWindow?) {
        window?.subviews.forEach { $0.removeFromSuperview() }
        window?.rootViewController = nil
        window?.rootViewController = R.storyboard.login.instantiateInitialViewController()
        window?.makeKeyAndVisible()
    }

    func showMainApp(withWindow window: UIWindow?) {
        window?.rootViewController = nil
        window?.rootViewController = R.storyboard.mainTabBar.instantiateInitialViewController()
        window?.makeKeyAndVisible()
    }

}

3) Então eu posso conformar meu AppDelegate ao protocolo LoginFlowHandler e chamar o handleLogin na inicialização:

class AppDelegate: UIResponder, UIApplicationDelegate, LoginFlowHandler {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow.init(frame: UIScreen.main.bounds)

        initialiseServices()

        handleLogin(withWindow: window)

        return true
    }

}

A partir daqui, minha extensão de protocolo manipulará a lógica ou determinará se o usuário está logado / desconectado e, em seguida, alterará o windows rootViewController de acordo!


Aqui está o que acabei fazendo para realizar tudo. A única coisa que você precisa considerar além disso é (a) o processo de login e (b) onde você está armazenando os dados do seu aplicativo (neste caso, eu usei um singleton).

Como você pode ver, o controlador de visualização raiz é meu Controlador da Guia Principal . Eu fiz isso porque depois que o usuário fez login, quero que o aplicativo seja iniciado diretamente na primeira guia. (Isso evita qualquer "flicker", onde a visão de login mostra temporariamente.)

AppDelegate.m

Neste arquivo, eu verifico se o usuário já está logado. Se não, eu pressiono o controlador de visualização de login. Também cuido do processo de logout, onde limpo os dados e mostro a visualização de login.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

    // Show login view if not logged in already
    if(![AppData isLoggedIn]) {
        [self showLoginScreen:NO];
    }

    return YES;
}

-(void) showLoginScreen:(BOOL)animated
{

    // Get login screen from storyboard and present it
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    LoginViewController *viewController = (LoginViewController *)[storyboard instantiateViewControllerWithIdentifier:@"loginScreen"];
    [self.window makeKeyAndVisible];
    [self.window.rootViewController presentViewController:viewController
                                             animated:animated
                                           completion:nil];
}

-(void) logout
{
    // Remove data from singleton (where all my app data is stored)
    [AppData clearData];

   // Reset view controller (this will quickly clear all the views)
   UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
   MainTabControllerViewController *viewController = (MainTabControllerViewController *)[storyboard instantiateViewControllerWithIdentifier:@"mainView"];
   [self.window setRootViewController:viewController];

   // Show login screen
   [self showLoginScreen:NO];

}

LoginViewController.m

Aqui, se o login for bem-sucedido, simplesmente descartarei a visualização e enviarei uma notificação.

-(void) loginWasSuccessful
{

     // Send notification
     [[NSNotificationCenter defaultCenter] postNotificationName:@"loginSuccessful" object:self];

     // Dismiss login screen
     [self dismissViewControllerAnimated:YES completion:nil];

}

Eu não gostei da resposta do bhavya por causa do uso do AppDelegate dentro dos View rootViewController e a configuração do rootViewController não tem animação. E a resposta de Trevor tem problema com o controlador de visualização intermitente no iOS8.

UPD 07/18/2015

AppDelegate dentro de Controladores View:

Alterar o estado AppDelegate (propriedades) dentro do controlador de exibição interrompe o encapsulamento.

Hierarquia muito simples de objetos em todos os projetos do iOS:

AppDelegate (possui a window e rootViewController )

ViewController (possui view )

Tudo bem que objetos do topo mudem objetos na parte inferior, porque eles estão criando. Mas não está ok se os objetos na parte inferior mudarem objetos em cima deles (eu descrevi alguns princípios básicos de programação / OOP: DIP (Princípio de Inversão de Dependência: módulo de alto nível não deve depender do módulo de baixo nível, mas eles dependem de abstrações) ).

Se algum objeto alterar qualquer objeto nessa hierarquia, mais cedo ou mais tarde haverá uma confusão no código. Pode ser ok nos pequenos projetos, mas não é divertido cavar essa bagunça nos projetos bit =]

UPD 07/18/2015

Eu replicar animações de controlador modal usando UINavigationController (tl; dr: check the project ).

Estou usando o UINavigationController para apresentar todos os controladores no meu aplicativo. Inicialmente exibi o controlador de visualização de login na pilha de navegação com animação push / pop simples. Do que eu decidi alterá-lo para modal com alterações mínimas.

Como funciona:

  1. O controlador de visualização inicial (ou self.window.rootViewController ) é o UINavigationController com ProgressViewController como um rootViewController . Eu estou mostrando ProgressViewController porque DataModel pode levar algum tempo para inicializar porque ele inits pilha de dados do núcleo, como neste article (eu realmente gosto dessa abordagem).

  2. AppDelegate é responsável por obter atualizações de status de login.

  3. O DataModel manipula o login / logout do usuário e o AppDelegate está observando sua propriedade userLoggedIn via KVO. Indiscutivelmente não é o melhor método para fazer isso, mas funciona para mim. (Por que KVO é ruim, você pode verificar this ou neste artigo (Por que não usar Notificações? Parte).

  4. ModalDismissAnimator e ModalPresentAnimator são usados ​​para personalizar a animação de push padrão.

Como a lógica dos animadores funciona:

  1. AppDelegate define-se como um delegado de self.window.rootViewController (que é UINavigationController).

  2. AppDelegate retorna um dos animadores em -[AppDelegate navigationController:animationControllerForOperation:fromViewController:toViewController:] se necessário.

  3. Os animadores implementam -animateTransition: métodos -transitionDuration: e -animateTransition: methods. -[ModalPresentAnimator animateTransition:] :

    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
        UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        [[transitionContext containerView] addSubview:toViewController.view];
        CGRect frame = toViewController.view.frame;
        CGRect toFrame = frame;
        frame.origin.y = CGRectGetHeight(frame);
        toViewController.view.frame = frame;
        [UIView animateWithDuration:[self transitionDuration:transitionContext]
                         animations:^
         {
             toViewController.view.frame = toFrame;
         } completion:^(BOOL finished)
         {
             [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
         }];
    }

O projeto de teste está project .


Eu tive um problema semelhante para resolver em um aplicativo e usei o método a seguir. Não usei notificações para lidar com a navegação.

Eu tenho três storyboards no aplicativo.

  1. Storyboard da tela inicial - para inicialização do aplicativo e verificação se o usuário já está conectado
  2. Storyboard de login - para gerenciar o fluxo de login do usuário
  3. Storyboard da barra de guias - para exibir o conteúdo do aplicativo

Meu storyboard inicial no aplicativo é o storyboard da tela inicial. Eu tenho o controlador de navegação como a raiz do login e do storyboard da barra de guias para lidar com as navegações do controlador de visualização.

Eu criei uma classe Navigator para lidar com a navegação do aplicativo e se parece com isso:

class Navigator: NSObject {

   static func moveTo(_ destinationViewController: UIViewController, from sourceViewController: UIViewController, transitionStyle: UIModalTransitionStyle? = .crossDissolve, completion: (() -> ())? = nil) {
       

       DispatchQueue.main.async {

           if var topController = UIApplication.shared.keyWindow?.rootViewController {

               while let presentedViewController = topController.presentedViewController {

                   topController = presentedViewController

               }

               
               destinationViewController.modalTransitionStyle = (transitionStyle ?? nil)!

               sourceViewController.present(destinationViewController, animated: true, completion: completion)

           }

       }

   }

}

Vamos ver os possíveis cenários:

  • Primeiro lançamento do aplicativo; A tela inicial será carregada, onde eu verifico se o usuário já está conectado. Em seguida, a tela de login será carregada usando a classe Navigator da seguinte maneira;

Como tenho o controlador de navegação como raiz, instancio o controlador de navegação como controlador de visualização inicial.

let loginSB = UIStoryboard(name: "splash", bundle: nil)

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)

Isso remove o storyboard de slpash da raiz da janela do aplicativo e o substitui pelo storyboard de login.

A partir do storyboard de login, quando o usuário tiver efetuado login com êxito, salvarei os dados do usuário em Padrões do usuário e inicializarei um singleton UserData para acessar os detalhes do usuário. Em seguida, o storyboard da Barra de guias é carregado usando o método do navegador.

Let tabBarSB = UIStoryboard(name: "tabBar", bundle: nil)
let tabBarNav = tabBarSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(tabBarNav, from: self)

Agora, o usuário sai da tela de configurações na barra de guias. Limpo todos os dados do usuário salvos e navego para a tela de login.

let loginSB = UIStoryboard(name: "splash", bundle: nil)

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)
  • Usuário está logado e força mata o aplicativo

Quando o usuário inicia o aplicativo, a tela inicial será carregada. Eu verifico se o usuário está logado e acesso aos dados do usuário a partir dos padrões do usuário. Em seguida, inicialize o singleton UserData e mostra a barra de guias em vez da tela de login.


Fazer isso com o delegado do aplicativo NÃO é recomendado. O AppDelegate gerencia o ciclo de vida do aplicativo relacionado ao lançamento, suspensão, finalização e assim por diante. Sugiro fazer isso a partir do seu controlador de visualização inicial no viewDidAppear . Você pode self.presentViewController e self.dismissViewController do controlador de visualização de login. Armazene uma chave bool em NSUserDefaults para ver se está sendo lançada pela primeira vez.


No Xcode 7, você pode ter vários storyBoards. Será melhor se você conseguir manter o fluxo de Login em um storyboard separado.

Isso pode ser feito usando SELECT VIEWCONTROLLER> Editor> Refactor to Storyboard

E aqui está a versão Swift para definir uma visão como o RootViewContoller-

    let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    appDelegate.window!.rootViewController = newRootViewController

    let rootViewController: UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("LoginViewController")

insira a descrição da imagem aqui

Em App Delegate.m

 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
                                                     forBarMetrics:UIBarMetricsDefault];

NSString *identifier;
BOOL isSaved = [[NSUserDefaults standardUserDefaults] boolForKey:@"loginSaved"];
if (isSaved)
{
    //[email protected]"homeViewControllerId";
    UIWindow* mainWindow=[[[UIApplication sharedApplication] delegate] window];
    UITabBarController *tabBarVC =
    [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"TabBarVC"];
    mainWindow.rootViewController=tabBarVC;
}
else
{


    identifier[email protected]"loginViewControllerId";
    UIStoryboard *    storyboardobj=[UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:identifier];

    UINavigationController *navigationController=[[UINavigationController alloc] initWithRootViewController:screen];

    self.window.rootViewController = navigationController;
    [self.window makeKeyAndVisible];

}

return YES;

}

ver controller.m Na visão fez carregar

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

UIBarButtonItem* barButton = [[UIBarButtonItem alloc] initWithTitle:@"Logout" style:UIBarButtonItemStyleDone target:self action:@selector(logoutButtonClicked:)];
[self.navigationItem setLeftBarButtonItem:barButton];

}

Na ação do botão de logout

-(void)logoutButtonClicked:(id)sender{

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:@"Do you want to logout?" preferredStyle:UIAlertControllerStyleAlert];

    [alertController addAction:[UIAlertAction actionWithTitle:@"Logout" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
           NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setBool:NO forKey:@"loginSaved"];
           [[NSUserDefaults standardUserDefaults] synchronize];
      AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    UIStoryboard *    storyboardobj=[UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:@"loginViewControllerId"];
    [appDelegate.window setRootViewController:screen];
}]];


[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
    [self dismissViewControllerAnimated:YES completion:nil];
}]];

dispatch_async(dispatch_get_main_queue(), ^ {
    [self presentViewController:alertController animated:YES completion:nil];
});}

Depois de criar o LoginViewController e o TabBarController , precisamos adicionar um StoryboardID como “ loginViewController ” e “ tabBarController ” respectivamente.

Então eu prefiro criar a estrutura Constant :

struct Constants {
    struct StoryboardID {
        static let signInViewController = "SignInViewController"
        static let mainTabBarController = "MainTabBarController"
    }

    struct kUserDefaults {
        static let isSignIn = "isSignIn"
    }
}

Em LoginViewController, adicione o IBAction :

@IBAction func tapSignInButton(_ sender: UIButton) {
    UserDefaults.standard.set(true, forKey: Constants.kUserDefaults.isSignIn)
    Switcher.updateRootViewController()
}

Em ProfileViewController, adicione o IBAction :

@IBAction func tapSignOutButton(_ sender: UIButton) {
    UserDefaults.standard.set(false, forKey: Constants.kUserDefaults.isSignIn)
    Switcher.updateRootViewController()
}

Em AppDelegate, adicione linha de código em didFinishLaunchingWithOptions :

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    Switcher.updateRootViewController()

    return true
}

Finalmente crie uma classe Switcher :

import UIKit

class Switcher {

    static func updateRootViewController() {

        let status = UserDefaults.standard.bool(forKey: Constants.kUserDefaults.isSignIn)
        var rootViewController : UIViewController?

        #if DEBUG
        print(status)
        #endif

        if (status == true) {
            let mainStoryBoard = UIStoryboard(name: "Main", bundle: nil)
            let mainTabBarController = mainStoryBoard.instantiateViewController(withIdentifier: Constants.StoryboardID.mainTabBarController) as! MainTabBarController
            rootViewController = mainTabBarController
        } else {
            let mainStoryBoard = UIStoryboard(name: "Main", bundle: nil)
            let signInViewController = mainStoryBoard.instantiateViewController(withIdentifier: Constants.StoryboardID.signInViewController) as! SignInViewController
            rootViewController = signInViewController
        }

        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.window?.rootViewController = rootViewController

    }

}

Isso é tudo!


No seu appDelegate.m dentro do seu didFinishLaunchingWithOptions

//authenticatedUser: check from NSUserDefaults User credential if its present then set your navigation flow accordingly

if (authenticatedUser) 
{
    self.window.rootViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateInitialViewController];        
}
else
{
    UIViewController* rootController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"LoginViewController"];
    UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:rootController];

    self.window.rootViewController = navigation;
}

No arquivo SignUpViewController.m

- (IBAction)actionSignup:(id)sender
{
    AppDelegate *appDelegateTemp = [[UIApplication sharedApplication]delegate];

    appDelegateTemp.window.rootViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateInitialViewController];
}

No arquivo MyTabThreeViewController.m

- (IBAction)actionLogout:(id)sender {

    // Delete User credential from NSUserDefaults and other data related to user

    AppDelegate *appDelegateTemp = [[UIApplication sharedApplication]delegate];

    UIViewController* rootController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"LoginViewController"];

    UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:rootController];
    appDelegateTemp.window.rootViewController = navigation;

}

Versão Swift 4

didFinishLaunchingWithOptions no delegado do aplicativo assumindo que seu controlador de visualização inicial é o TabbarController assinado.

if Auth.auth().currentUser == nil {
        let rootController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "WelcomeNavigation")
        self.window?.rootViewController = rootController
    }

    return true

No controlador de exibição de inscrição:

@IBAction func actionSignup(_ sender: Any) {
let appDelegateTemp = UIApplication.shared.delegate as? AppDelegate
appDelegateTemp?.window?.rootViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController()
}

MyTabThreeViewController

 //Remove user credentials
guard let appDel = UIApplication.shared.delegate as? AppDelegate else { return }
        let rootController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "WelcomeNavigation")
        appDel.window?.rootViewController = rootController




clear