我正在使用Storyboard构建一个iOS应用程序。根视图控制器是一个标签栏控制器。我正在创建登录/注销过程,它基本上工作正常,但我有一些问题。我需要知道最好的方法来设置这一切。

我想做到以下几点:

在应用程序第一次启动时显示登录屏幕。当他们登录时,转到标签栏控制器的第一个标签。 任何时候他们启动应用程序之后,检查他们是否登录,并直接跳到根标签栏控制器的第一个标签。 当他们手动单击登出按钮时,显示登录屏幕,并清除视图控制器中的所有数据。

到目前为止,我所做的是将根视图控制器设置为标签栏控制器,并创建了一个自定义segue到Login视图控制器。在我的标签栏控制器类中,我检查他们是否在viewDidAppear方法中登录,并执行segue: [self performSegueWithIdentifier:@"pushLogin" sender:self];

我还设置了一个通知,当注销操作需要执行:[[NSNotificationCenter defaultCenter] addObserver:自我选择器:@选择器(logoutAccount)名称:@“logoutAccount”对象:nil];

注销后,我从Keychain中清除凭据,运行[self setSelectedIndex:0],并执行segue再次显示登录视图控制器。

这一切都很好,但我想知道:这个逻辑应该在AppDelegate中吗?我还有两个问题:

他们第一次启动应用程序时,标签栏控制器在segue执行之前简要显示。我已经尝试移动代码到viewWillAppear,但segue不会工作那么早。 注销时,所有数据仍在所有视图控制器中。如果他们登录到一个新帐户,旧帐户数据仍然显示,直到他们刷新。我需要一种方法来清除这很容易登出。

我愿意重新修改。我考虑过让登录屏幕成为根视图控制器,或者在AppDelegate中创建一个导航控制器来处理所有事情…我只是不确定目前最好的方法是什么。


当前回答

我不喜欢bhavya的答案,因为在视图控制器内使用AppDelegate和设置rootViewController没有动画。Trevor的回答是关于iOS8上闪烁视图控制器的问题。

UPD 07/18/2015

视图控制器内部的AppDelegate:

在视图控制器内更改AppDelegate状态(属性)会破坏封装。

每个iOS项目中非常简单的对象层次结构:

AppDelegate(拥有窗口和rootViewController)

ViewController(拥有视图)

顶部的对象可以改变底部的对象,因为它们正在创建这些对象。但是如果底部的对象改变它们上面的对象是不行的(我描述了一些基本的编程/OOP原则:DIP(依赖倒置原则:高级模块不能依赖于低级模块,但它们应该依赖于抽象))。

如果任何对象将改变这个层次结构中的任何对象,那么代码中迟早会出现混乱。在小项目上可能没问题,但在小项目上挖掘这个混乱是没有乐趣的=]

UPD 07/18/2015

我复制模态控制器动画使用UINavigationController (tl;dr:检查项目)。

我使用UINavigationController在我的应用程序中呈现所有控制器。最初,我在导航堆栈中显示登录视图控制器与普通的推送/弹出动画。然后我决定用最小的变化把它改成模态。

工作原理:

Initial view controller (or self.window.rootViewController) is UINavigationController with ProgressViewController as a rootViewController. I'm showing ProgressViewController because DataModel can take some time to initialize because it inits core data stack like in this article (I really like this approach). AppDelegate is responsible for getting login status updates. DataModel handles user login/logout and AppDelegate is observing it's userLoggedIn property via KVO. Arguably not the best method to do this but it works for me. (Why KVO is bad, you can check in this or this article (Why Not Use Notifications? part). ModalDismissAnimator and ModalPresentAnimator are used to customize default push animation.

动画师的逻辑工作原理:

AppDelegate sets itself as a delegate of self.window.rootViewController (which is UINavigationController). AppDelegate returns one of animators in -[AppDelegate navigationController:animationControllerForOperation:fromViewController:toViewController:] if necessary. Animators implement -transitionDuration: and -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]]; }]; }

测试项目在这里。

其他回答

我用这个来检查第一次发射:

- (NSInteger) checkForFirstLaunch
{
    NSInteger result = 0; //no first launch

    // Get current version ("Bundle Version") from the default Info.plist file
    NSString *currentVersion = (NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
    NSArray *prevStartupVersions = [[NSUserDefaults standardUserDefaults] arrayForKey:@"prevStartupVersions"];
    if (prevStartupVersions == nil)
    {
        // Starting up for first time with NO pre-existing installs (e.g., fresh
        // install of some version)
        [[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:currentVersion] forKey:@"prevStartupVersions"];
        result = 1; //first launch of the app
    } else {
        if (![prevStartupVersions containsObject:currentVersion])
        {
            // Starting up for first time with this version of the app. This
            // means a different version of the app was alread installed once
            // and started.
            NSMutableArray *updatedPrevStartVersions = [NSMutableArray arrayWithArray:prevStartupVersions];
            [updatedPrevStartVersions addObject:currentVersion];
            [[NSUserDefaults standardUserDefaults] setObject:updatedPrevStartVersions forKey:@"prevStartupVersions"];
            result = 2; //first launch of this version of the app
        }
    }

    // Save changes to disk
    [[NSUserDefaults standardUserDefaults] synchronize];

    return result;
}

(如果用户删除应用程序并重新安装,则算作第一次启动)

在AppDelegate中,我检查了第一次启动,并创建了一个带有登录屏幕(登录和注册)的导航控制器,我把它放在当前主窗口的顶部:

[self.window makeKeyAndVisible];

if (firstLaunch == 1) {
    UINavigationController *_login = [[UINavigationController alloc] initWithRootViewController:loginController];
    [self.window.rootViewController presentViewController:_login animated:NO completion:nil];
}

因为它在常规视图控制器的顶部它独立于应用的其他部分如果你不再需要它,你可以解散视图控制器。如果用户手动按下按钮,您也可以以这种方式显示视图。

顺便说一句:我保存用户的登录数据是这样的:

KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithIdentifier:@"com.youridentifier" accessGroup:nil];
[keychainItem setObject:password forKey:(__bridge id)(kSecValueData)];
[keychainItem setObject:email forKey:(__bridge id)(kSecAttrAccount)];

对于注销:我从CoreData(太慢)切换到现在使用nsarray和nsdictionary来管理我的数据。注销仅仅意味着清空这些数组和字典。另外,我确保在viewWillAppear中设置我的数据。

就是这样。

下面是我最终完成所有事情的方法。除此之外,你需要考虑的唯一一件事是(a)登录过程和(b)存储应用数据的位置(在本例中,我使用了单例)。

如你所见,根视图控制器是我的主选项卡控制器。我这样做是因为用户登录后,我希望应用程序直接启动到第一个选项卡。(这避免了登录视图临时显示的任何“闪烁”。)

AppDelegate.m

在这个文件中,我检查用户是否已经登录。如果不是,我就推登录视图控制器。我还处理注销过程,清除数据并显示登录视图。

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

在这里,如果登录成功,我只需取消视图并发送通知。

-(void) loginWasSuccessful
{

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

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

}

我在一个应用程序中也有类似的问题要解决,我使用了以下方法。我没有使用通知来处理导航。

我在应用程序中有三个故事板。

启动画面故事板-用于应用程序初始化和检查用户是否已经登录 登录故事板——用于处理用户登录流程 标签栏故事板-用于显示应用程序内容

我在应用程序中的初始故事板是启动画面故事板。 我有导航控制器作为根登录和选项卡栏故事板处理视图控制器导航。

我创建了一个Navigator类来处理应用程序导航,它看起来像这样:

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)

           }

       }

   }

}

让我们来看看可能的场景:

首次应用发行; 启动屏幕将加载,我检查用户是否已经登录。然后登录屏幕将使用Navigator类加载,如下所示;

因为我有导航控制器作为根,我实例化导航控制器作为初始视图控制器。

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

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)

这将从应用程序窗口的根目录中删除slpash故事板,并将其替换为登录故事板。

在登录故事板中,当用户成功登录时,我将用户数据保存到user Defaults中,并初始化UserData单例来访问用户详细信息。然后使用navigator方法加载标签栏故事板。

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

Navigator.moveTo(tabBarNav, from: self)

现在用户从标签栏的设置屏幕中退出。我清除所有保存的用户数据,并导航到登录屏幕。

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

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)

用户登录并强制杀死应用程序

当用户启动应用程序时,启动画面将被加载。我检查用户是否已登录,并从用户默认中访问用户数据。然后初始化UserData单例,显示标签栏而不是登录屏幕。

感谢bhavya的解决方案。关于swift,有两种答案,但都不是很完整。我已经用swift3做过了。下面是主要代码。

在AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    // seclect the mainStoryBoard entry by whthere user is login.
    let userDefaults = UserDefaults.standard

    if let isLogin: Bool = userDefaults.value(forKey:Common.isLoginKey) as! Bool? {
        if (!isLogin) {
            self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LogIn")
        }
   }else {
        self.window?.rootViewController = mainStoryboard.instantiateViewController(withIdentifier: "LogIn")
   }

    return true
}

在SignUpViewController.swift

@IBAction func userLogin(_ sender: UIButton) {
    //handle your login work
    UserDefaults.standard.setValue(true, forKey: Common.isLoginKey)
    let delegateTemp = UIApplication.shared.delegate
    delegateTemp?.window!?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Main")
}

在logOutAction函数中

@IBAction func logOutAction(_ sender: UIButton) {
    UserDefaults.standard.setValue(false, forKey: Common.isLoginKey)
    UIApplication.shared.delegate?.window!?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
}

我不喜欢bhavya的答案,因为在视图控制器内使用AppDelegate和设置rootViewController没有动画。Trevor的回答是关于iOS8上闪烁视图控制器的问题。

UPD 07/18/2015

视图控制器内部的AppDelegate:

在视图控制器内更改AppDelegate状态(属性)会破坏封装。

每个iOS项目中非常简单的对象层次结构:

AppDelegate(拥有窗口和rootViewController)

ViewController(拥有视图)

顶部的对象可以改变底部的对象,因为它们正在创建这些对象。但是如果底部的对象改变它们上面的对象是不行的(我描述了一些基本的编程/OOP原则:DIP(依赖倒置原则:高级模块不能依赖于低级模块,但它们应该依赖于抽象))。

如果任何对象将改变这个层次结构中的任何对象,那么代码中迟早会出现混乱。在小项目上可能没问题,但在小项目上挖掘这个混乱是没有乐趣的=]

UPD 07/18/2015

我复制模态控制器动画使用UINavigationController (tl;dr:检查项目)。

我使用UINavigationController在我的应用程序中呈现所有控制器。最初,我在导航堆栈中显示登录视图控制器与普通的推送/弹出动画。然后我决定用最小的变化把它改成模态。

工作原理:

Initial view controller (or self.window.rootViewController) is UINavigationController with ProgressViewController as a rootViewController. I'm showing ProgressViewController because DataModel can take some time to initialize because it inits core data stack like in this article (I really like this approach). AppDelegate is responsible for getting login status updates. DataModel handles user login/logout and AppDelegate is observing it's userLoggedIn property via KVO. Arguably not the best method to do this but it works for me. (Why KVO is bad, you can check in this or this article (Why Not Use Notifications? part). ModalDismissAnimator and ModalPresentAnimator are used to customize default push animation.

动画师的逻辑工作原理:

AppDelegate sets itself as a delegate of self.window.rootViewController (which is UINavigationController). AppDelegate returns one of animators in -[AppDelegate navigationController:animationControllerForOperation:fromViewController:toViewController:] if necessary. Animators implement -transitionDuration: and -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]]; }]; }

测试项目在这里。