ChatGPT解决这个技术问题 Extra ChatGPT

Best practices for Storyboard login screen, handling clearing of data upon logout

I'm building an iOS app using a Storyboard. The root view controller is a Tab Bar Controller. I'm creating the login/logout process, and it's mostly working fine, but I've got a few issues. I need to know the BEST way to set all this up.

I want to accomplish the following:

Show a login screen the first time the app is launched. When they login, go to the first tab of the Tab Bar Controller. Any time they launch the app after that, check if they are logged in, and skip straight to the first tab of the root Tab Bar Controller. When they manually click a logout button, show the login screen, and clear all the data from the view controllers.

What I've done so far is set the root view controller to the Tab Bar Controller, and created a custom segue to my Login view controller. Inside my Tab Bar Controller class, I check whether they are logged in inside the viewDidAppear method, and a perform the segue: [self performSegueWithIdentifier:@"pushLogin" sender:self];

I also setup a notification for when the logout action needs to be performed: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(logoutAccount) name:@"logoutAccount" object:nil];

Upon logout, I clear the credentials from the Keychain, run [self setSelectedIndex:0] and perform the segue to show the login view controller again.

This all works fine, but I'm wondering: should this logic be in the AppDelegate? I also have two issues:

The first time they launch the app, the Tab Bar Controller shows briefly before the segue is performed. I've tried moving the code to viewWillAppear but the segue will not work that early.

When they logout, all the data is still inside all the view controllers. If they login to a new account, the old account data is still displayed until they refresh. I need a way to clear this easily on logout.

I'm open to reworking this. I've considered making the login screen the root view controller, or creating a navigation controller in the AppDelegate to handle everything... I'm just not sure what the best method is at this point.

Do you present login view controller as modal?
@TrevorGehman - can add your storyboard pic
I submitted an answer with the details of what I ended up doing. It's similar to some of the other answers provided, especially @bhavya kothari.
For the presenting the login screen, AuthNavigation may be useful. It organizes the presentation of a login screen if needed and also supports auto-login.
One of the very basic problems which is almost always solved but at the same time feels like could have been done better

J
Joseph Francis

https://i.stack.imgur.com/KUKCP.png

In your appDelegate.m inside your 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;
}

In SignUpViewController.m file

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

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

In file 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;

}

Swift 4 Version

didFinishLaunchingWithOptions in app delegate assuming your initial view controller is the signed in TabbarController.

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

    return true

In Sign up view controller:

@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

You forgot deleting bool authentication from userDefaults after logout
-1 for using AppDelegate inside UIViewController and setting window.rootViewController there. I don't consider this as a "best practice".
Didn't want give -1 without posting an answer: stackoverflow.com/a/30664935/1226304
Im trying to do this in swift on IOS8 but I get the following error when the app fires up and the login screen shows: "Unbalanced calls to begin/end appearance transitions". I have noticed that when the app loads the login screen shows, but also the first tab on the tab bar controller is getting loaded also. Confirmed this via println() in viewdidload. Suggestions?
bingo! -2. -1 for AppDelegate inside UIViewController -1 for Storing login key in NSUserDefaults. It's very-very insecure for that kind of data!
T
Trevor Gehman

Here is what I ended up doing to accomplish everything. The only thing you need to consider in addition to this is (a) the login process and (b) where you are storing your app data (in this case, I used a singleton).

https://i.stack.imgur.com/rNQMA.png

As you can see, the root view controller is my Main Tab Controller. I did this because after the user has logged in, I want the app to launch directly to the first tab. (This avoids any "flicker" where the login view shows temporarily.)

AppDelegate.m

In this file, I check whether the user is already logged in. If not, I push the login view controller. I also handle the logout process, where I clear data and show the login view.

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

Here, if the login is successful, I simply dismiss the view and send a notification.

-(void) loginWasSuccessful
{

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

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

}

What do you use the notification for?
In iOS 8.1 (and perhaps 8.0, haven't tested) this no longer works smoothly. The initial View Controller flashes for a brief moment.
Is there a Swift version of this approach?
@Seano yes. Translate the code you see above to the different syntax. The APIs are exactly the same. There is no difference.
@Julian In iOS 8, I replace the two lines [self.window makeKeyAndVisible]; [self.window.rootViewController presentViewController:viewController animated:animated completion:nil]; with self.window.rootViewController = viewController; to prevent the flicker. To animate that just wrap it in a [UIView transitionWithView...];
D
Dimitris Bouzikas

EDIT: Add logout action.

https://i.stack.imgur.com/ZLlkk.jpg

1. First of all prepare the app delegate file

AppDelegate.h

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (nonatomic) BOOL authenticated;

@end

AppDelegate.m

#import "AppDelegate.h"
#import "User.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    User *userObj = [[User alloc] init];
    self.authenticated = [userObj userAuthenticated];

    return YES;
}

2. Create a class named User.

User.h

#import <Foundation/Foundation.h>

@interface User : NSObject

- (void)loginWithUsername:(NSString *)username andPassword:(NSString *)password;
- (void)logout;
- (BOOL)userAuthenticated;

@end

User.m

#import "User.h"

@implementation User

- (void)loginWithUsername:(NSString *)username andPassword:(NSString *)password{

    // Validate user here with your implementation
    // and notify the root controller
    [[NSNotificationCenter defaultCenter] postNotificationName:@"loginActionFinished" object:self userInfo:nil];
}

- (void)logout{
    // Here you can delete the account
}

- (BOOL)userAuthenticated {

    // This variable is only for testing
    // Here you have to implement a mechanism to manipulate this
    BOOL auth = NO;

    if (auth) {
        return YES;
    }

    return NO;
}

3. Create a new controller RootViewController and connected with the first view, where login button live. Add also a Storyboard ID: "initialView".

RootViewController.h

#import <UIKit/UIKit.h>
#import "LoginViewController.h"

@protocol LoginViewProtocol <NSObject>

- (void)dismissAndLoginView;

@end

@interface RootViewController : UIViewController

@property (nonatomic, weak) id <LoginViewProtocol> delegate;
@property (nonatomic, retain) LoginViewController *loginView;


@end

RootViewController.m

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

@synthesize loginView;

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)loginBtnPressed:(id)sender {

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(loginActionFinished:)
                                                 name:@"loginActionFinished"
                                               object:loginView];

}

#pragma mark - Dismissing Delegate Methods

-(void) loginActionFinished:(NSNotification*)notification {

    AppDelegate *authObj = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    authObj.authenticated = YES;

    [self dismissLoginAndShowProfile];
}

- (void)dismissLoginAndShowProfile {
    [self dismissViewControllerAnimated:NO completion:^{
        UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        UITabBarController *tabView = [storyboard instantiateViewControllerWithIdentifier:@"profileView"];
        [self presentViewController:tabView animated:YES completion:nil];
    }];


}

@end

4. Create a new controller LoginViewController and connected with the login view.

LoginViewController.h

#import <UIKit/UIKit.h>
#import "User.h"

@interface LoginViewController : UIViewController

LoginViewController.m

#import "LoginViewController.h"
#import "AppDelegate.h"

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (IBAction)submitBtnPressed:(id)sender {
    User *userObj = [[User alloc] init];

    // Here you can get the data from login form
    // and proceed to authenticate process
    NSString *username = @"username retrieved through login form";
    NSString *password = @"password retrieved through login form";
    [userObj loginWithUsername:username andPassword:password];
}

@end

5. At the end add a new controller ProfileViewController and connected with the profile view in the tabViewController.

ProfileViewController.h

#import <UIKit/UIKit.h>

@interface ProfileViewController : UIViewController

@end

ProfileViewController.m

#import "ProfileViewController.h"
#import "RootViewController.h"
#import "AppDelegate.h"
#import "User.h"

@interface ProfileViewController ()

@end

@implementation ProfileViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

}

- (void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    if(![(AppDelegate*)[[UIApplication sharedApplication] delegate] authenticated]) {

        UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

        RootViewController *initView =  (RootViewController*)[storyboard instantiateViewControllerWithIdentifier:@"initialView"];
        [initView setModalPresentationStyle:UIModalPresentationFullScreen];
        [self presentViewController:initView animated:NO completion:nil];
    } else{
        // proceed with the profile view
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)logoutAction:(id)sender {

   User *userObj = [[User alloc] init];
   [userObj logout];

   AppDelegate *authObj = (AppDelegate*)[[UIApplication sharedApplication] delegate];
   authObj.authenticated = NO;

   UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

   RootViewController *initView =  (RootViewController*)[storyboard instantiateViewControllerWithIdentifier:@"initialView"];
   [initView setModalPresentationStyle:UIModalPresentationFullScreen];
   [self presentViewController:initView animated:NO completion:nil];

}

@end

LoginExample is a sample project for extra help.


sample project helped me very much to understand the concept of login n logout.. many thanks :)
S
Souhaib Guitouni

I didn't like bhavya's answer because of using AppDelegate inside View Controllers and setting rootViewController has no animation. And Trevor's answer has issue with flashing view controller on iOS8.

UPD 07/18/2015

AppDelegate inside View Controllers:

Changing AppDelegate state (properties) inside view controller breaks encapsulation.

Very simple hierarchy of objects in every iOS project:

AppDelegate (owns window and rootViewController)

ViewController (owns view)

It's ok that objects from the top change objects at the bottom, because they are creating them. But it's not ok if objects on the bottom change objects on top of them (I described some basic programming/OOP principle : DIP (Dependency Inversion Principle : high level module must not depend on the low level module, but they should depend on abstractions)).

If any object will change any object in this hierarchy, sooner or later there will be a mess in the code. It might be ok on the small projects but it's no fun to dig through this mess on the bit projects =]

UPD 07/18/2015

I replicate modal controller animations using UINavigationController (tl;dr: check the project).

I'm using UINavigationController to present all controllers in my app. Initially I displayed login view controller in navigation stack with plain push/pop animation. Than I decided to change it to modal with minimal changes.

How it works:

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.

How animators logic works:

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)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]]; }]; }

Test project is here.


Personally I have no problem with View Controllers knowing about AppDelegate (I'd be interested to understand why you do) - but your comment about lack of animation is very valid. That can be solved by this answer: stackoverflow.com/questions/8053832/…
@HughHughTeotl Thank you for the comment and for the link. I updated my answer.
@derpoliuk what if my base view controller is a UITabBarController? I can't push it in a UINavigationController.
@Giorgio, it's an interesting question, I didn't use UITabBarController for a very long time. I'd probably start with window approach instead of manipulating view controllers.
p
pkamb

Here's my Swifty solution for any future onlookers.

1) Create a protocol to handle both login and logout functions:

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

2) Extend said protocol and provide the functionality here for logging out:

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) Then I can conform my AppDelegate to the LoginFlowHandler protocol, and call handleLogin on startup:

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
    }

}

From here, my protocol extension will handle the logic or determining if the user if logged in/out, and then change the windows rootViewController accordingly!


Not sure if i'm being stupid but, AppDelegate doesn't conform to LoginFlowHandler. Am I missing something? Also, I'm guessing this code only manages log in on start up. How do I manage log out from a view controller?
@luke since all the logic is implemented in the extension there is no need to implement it in the AppDelegate. That's what so great in Protocol Extensions.
Sorry @sirFunkenstine, that was a custom class I created to show an example of how one would check their app cache to check a user has previously logged in or not. This AppState implementation would therefore depend on how you are saving your user data to disk.
@HarryBloom how would one use the handleLogout functionality?
Hi @nithinisreddy - to call the handleLogout functionality, you will need to conform the class that you are calling from to the LoginFlowHandler protocol. Then you will get scope to be able to call the handleLogout method. See my step 3 for an example of how I did that for the AppDelegate class.
M
Mihado

Doing this from the app delegate is NOT recommended. AppDelegate manages the app life cycle that relate to launching, suspending, terminating and so on. I suggest doing this from your initial view controller in the viewDidAppear. You can self.presentViewController and self.dismissViewController from the login view controller. Store a bool key in NSUserDefaults to see if it's launching for the first time.


Should the view appear (be visible to the user) in `viewDidAppear'? This will still create a flicker.
Not an answer. And "Store a bool key in NSUserDefaults to see if it's launching for the first time. " is very very dangerous for that kind of data.
i
iAleksandr

https://i.stack.imgur.com/4TMu3.png

After creating the LoginViewController and TabBarController, we need to add a StoryboardID as “loginViewController” and “tabBarController” respectively.

Then I prefer to create the Constant struct:

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

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

In LoginViewController add IBAction:

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

In ProfileViewController add IBAction:

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

In AppDelegate add line of code in didFinishLaunchingWithOptions:

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

    Switcher.updateRootViewController()

    return true
}

Finally create Switcher class:

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

    }

}

That is all!


Is there any difference which view controller is initial in storyboards? In your added photo i can see that u have option "is Initial View Controller" checked on Tab Bar Controller. In AppDelegate u switch main root view controller so i guess it doesn't matter , does it?
@iAleksandr Please update the answer for iOS 13. Coz of SceneDelegate current answer isn't working.
Hey Bro. Your Code isn't working when User Tapped on Sign Up. Please Add this Feature too..
M
Mahbub Morshed

In Xcode 7 you can have multiple storyBoards. It will be better if you can keep the Login flow in a separate storyboard.

This can be done using SELECT VIEWCONTROLLER > Editor > Refactor to Storyboard

And here is the Swift version for setting a view as the RootViewContoller-

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

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

T
Thorsten

I use this to check for first launch:

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

(if the user deletes the app and re-installs it, it counts like a first launch)

In the AppDelegate I check for first launch and create a navigation-controller with the login screens (login and register), which I put on top of the current main window:

[self.window makeKeyAndVisible];

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

As this is on top of the regular view controller it's independent from the rest of your app and you can just dismiss the view controller, if you don't need it anymore. And you can also present the view this way, if the user presses a button manually.

BTW: I save the login-data from my users like this:

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

For the logout: I switched away from CoreData (too slow) and use NSArrays and NSDictionaries to manage my data now. Logout just means to empty those arrays and dictionaries. Plus I make sure to set my data in viewWillAppear.

That's it.


N
Naresh

To update @iAleksandr answer for Xcode 11, which causes problems due to Scene kit.

Replace

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

With

guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,let sceneDelegate = windowScene.delegate as? SceneDelegate         else {
   return       
}
sceneDelegate.window?.rootViewController = rootViewController

call the Switcher.updateRootViewcontroller in Scene delegate rather than App delegate like this: func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { Switcher.updateRootViewController() guard let _ = (scene as? UIWindowScene) else { return } }


a
amb

I'm in the same situation as you and the solution I found for cleaning the data is deleting all the CoreData stuff that my view controllers rely on to draw it's info. But I still found this approach to be very bad, I think that a more elegant way to do this can be accomplished without storyboards and using only code to manage the transitions between view controllers.

I've found this project at Github that does all this stuff only by code and it's quite easy to understand. They use a Facebook-like side menu and what they do is change the center view controller depending if the user is logged-in or not. When the user logs out the appDelegate removes the data from CoreData and sets the main view controller to the login screen again.


4
4b0

I had a similar issue to solve in an app and I used the following method. I didn't use notifications for handling the navigation.

I have three storyboards in the app.

Splash screen storyboard - for app initialisation and checking if the user is already logged in Login storyboard - for handling user login flow Tab bar storyboard - for displaying the app content

My initial storyboard in the app is Splash screen storyboard. I have navigation controller as the root of login and tab bar storyboard to handle view controller navigations.

I created a Navigator class to handle the app navigation and it looks like this:

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)

           }

       }

   }

}

Let's look at the possible scenarios:

First app launch; Splash screen will be loaded where I check if the user is already signed in. Then login screen will be loaded using the Navigator class as follows;

Since I have navigation controller as the root, I instantiate the navigation controller as initial view controller.

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

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)

This removes the slpash storyboard from app window's root and replaces it with login storyboard.

From login storyboard, when the user is successfully logged in, I save the user data to User Defaults and initialize a UserData singleton to access the user details. Then Tab bar storyboard is loaded using the navigator method.

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

Navigator.moveTo(tabBarNav, from: self)

Now the user signs out from the settings screen in tab bar. I clear all the saved user data and navigate to login screen.

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

let loginNav = loginSB.instantiateInitialViewcontroller() as! UINavigationController

Navigator.moveTo(loginNav, from: self)

User is logged in and force kills the app

When user launches the app, Splash screen will be loaded. I check if user is logged in and access the user data from User Defaults. Then initialize the UserData singleton and shows tab bar instead of login screen.


W
WangYang

Thanks bhavya's solution.There have been two answers about swift, but those are not very intact. I have do that in the swift3.Below is the main code.

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

In 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")
}

In logOutAction function

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

Hi Eli. The question you answered already has a couple really good answers. When you decide to answer such a question please make sure to explain why your answer is better than the very good ones that were already posted.
Hi Noel. I noticed the other answers for swift. But I considered the answers are not very intact. So I submit my answer about swift3 version. It would be help for new swift programmer.Thank you!@Noel Widmer.
Can you add that explanation at the top of your post? That way everybody can immediatly see the benefit of your answer. Have a good time on SO! :)
Tanks for your suggest.I have added the explanation.Thanks again.@Noel Widmer.
Vague solution that does not highlight the use of 'Common' keyword.
u
user5575941

enter image description here

In 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)
{
    //identifier=@"homeViewControllerId";
    UIWindow* mainWindow=[[[UIApplication sharedApplication] delegate] window];
    UITabBarController *tabBarVC =
    [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"TabBarVC"];
    mainWindow.rootViewController=tabBarVC;
}
else
{


    identifier=@"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;

}

view controller.m In view did load

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

}

In logout button action

-(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];
});}

Why there is the need to add some functionality into the file ViewController.m ??
@Eesha He added a "logout" TabBar button item to the TabBar. I guess image is missing else you could have seen it.
Store login key in NSUserDefaults is very-very insecure for that kind of data!