298

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:

  1. Show a login screen the first time the app is launched. When they login, go to the first tab of the Tab Bar Controller.
  2. 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.
  3. 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.

braX
  • 11,506
  • 5
  • 20
  • 33
Trevor Gehman
  • 4,645
  • 3
  • 22
  • 25
  • Do you present login view controller as modal? – vokilam Feb 19 '14 at 09:16
  • @TrevorGehman - can add your storyboard pic – rohan k shah Feb 19 '14 at 17:50
  • 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. – Trevor Gehman Feb 19 '14 at 19:56
  • For the presenting the login screen, [AuthNavigation](https://github.com/columbbus/AuthNavigation) may be useful. It organizes the presentation of a login screen if needed and also supports auto-login. – Codey Apr 08 '18 at 17:48
  • One of the very basic problems which is almost always solved but at the same time feels like could have been done better – amar Sep 20 '18 at 04:50

14 Answers14

313

Your storyboard should look like this

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
Joseph Francis
  • 1,111
  • 1
  • 15
  • 26
bhavya kothari
  • 7,484
  • 4
  • 27
  • 53
  • You forgot deleting bool authentication from userDefaults after logout – CodeLover Nov 29 '14 at 07:28
  • 30
    -1 for using `AppDelegate` inside `UIViewController` and setting `window.rootViewController` there. I don't consider this as a "best practice". – derpoliuk Jun 05 '15 at 09:57
  • 2
    Didn't want give `-1` without posting an answer: http://stackoverflow.com/a/30664935/1226304 – derpoliuk Jun 05 '15 at 11:06
  • 1
    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? – Alex Lacayo Jun 27 '15 at 10:55
  • Note that the view switches without animation. Not best practice for iOS - but easily solved here: http://stackoverflow.com/questions/8053832/rootviewcontroller-animation-transition-initial-orientation-is-wrong – HughHughTeotl Jul 10 '15 at 22:24
  • Does this still cause a flicker when the user opens the app? – Jacob Jul 27 '15 at 02:51
  • And why it is the best answer? it doesn't decouple controllers from storyboards, it doesn't decouple login controllers and main controllers? – gaussblurinc Nov 12 '15 at 10:38
  • This answer doesn't allow animating the changes? – Markus Rautopuro Feb 11 '16 at 12:50
  • I though storing any kind of sensitive information (for example user credentials) inside NSUserDefaults is bad? And from AppDelegate it seems that you are doing exactly that, or I am confusing something here? – Xernox Mar 09 '16 at 16:41
  • Can you please check my question? http://stackoverflow.com/questions/36767816/why-not-just-set-initial-view-controller-for-login-screen-like-this – osrl Apr 21 '16 at 11:12
  • "Your storyboard should look like this" - image is expired. – Bista Sep 05 '16 at 04:27
  • 1
    bingo! -2. -1 for `AppDelegate` inside `UIViewController` -1 for Storing login key in `NSUserDefaults`. It's very-very insecure for that kind of data! – skywinder Aug 10 '17 at 17:02
  • Although I understand it, I kind of disagree with @derpoliuk. I can single handedly point out a plethora of Apps that does this, from the app store especially with "Splash Screens" becoming a thing and the actual Splash screens inability to execute any code developers resort to using the "First VC" as Splash Screen which often times calls many services etc and makes decision as to which Controller to be set as root. As far as animation is concerned while setting root, Swift 3/4 has made it a piece of cake to implement animations. – Anjan Biswas Feb 02 '18 at 23:20
  • @Annjawn I agree that splash screens are becoming more and more useful, especially now when we can use storyboards to design them. But my point was about using reference to app delegate from view controller, I explained it in more details in my answer: https://stackoverflow.com/a/30664935/1226304 – derpoliuk Feb 04 '18 at 09:02
97

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).

Storyboard showing login view controller and main tab controller

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

}
Trevor Gehman
  • 4,645
  • 3
  • 22
  • 25
  • 2
    What do you use the notification for? – rebellion Aug 24 '14 at 14:50
  • @ronnyandre, I am guessing that it is to notify the "mainView" to pull fresh data now that the user is logged in. That's how I am using it anyway. – BFeher Aug 29 '14 at 01:19
  • 1
    @BFeher is right. I used the notification to trigger a fresh data pull. You can use it to do whatever you want, but in my case, I needed to be notified that the login was successful, and fresh data was needed. – Trevor Gehman Aug 29 '14 at 16:57
  • 24
    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. – BFeher Oct 23 '14 at 07:17
  • seems an awesome explanation, how can I got the sample project about all of this? – developer Mar 01 '15 at 18:57
  • Hi. I tryed this approach but I have a problem. I still see the main view controller for about 1 second before I see the login controller. How can I walk around this? – Nuno Gonçalves Mar 24 '15 at 10:58
  • 7
    Is there a Swift version of this approach? – Sean H Apr 22 '15 at 17:13
  • +1 for your approach. Worked for me!! One thing, should we must set the rootViewController of the window inside the logout function? Because if I set an initialViewController in the storyboard, so it should be the rootViewController of the window automatically, right? Please correct me if I'm wrong!!! BTW, thanks for the approach!! :) – Erfan Jun 19 '15 at 11:24
  • Is there any way to fix the flicker of the initial VC that happens when the app opens? – Jacob Jul 27 '15 at 02:52
  • @BFeher you are right. It's not working anymore. I tried adding the view manually but it still look crap. – Julian F. Weinert Aug 20 '15 at 09:16
  • 2
    @Seano yes. Translate the code you see above to the different syntax. The APIs are exactly the same. There is no difference. – Julian F. Weinert Aug 20 '15 at 09:17
  • 10
    @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...];``` – BFeher Aug 21 '15 at 07:18
  • @Jacob see my comment above. This solved the flicker problem for me in iOS 8+. – BFeher Aug 21 '15 at 07:22
  • Nice, this does seem like a cleaner approach than the one in the highest-voted answer. Thanks! – elsurudo Oct 12 '15 at 18:31
  • 1
    if you call presentViewController on rootViewController in appDelegate, there is warning:Warning: Attempt to present on whose view is not in the window hierarchy! – Wingzero Oct 21 '15 at 08:51
  • 1
    @BFeher it seems with that solution then this line in LoginViewController no longer works: ` [self dismissViewControllerAnimated:YES completion:nil];` – Marcus Leon Aug 28 '16 at 12:35
  • @BFeher why are you not using `makeKeyAndVisible`? Isn't it needed every time you set your initial viewController *programmatically*? – mfaani Sep 18 '16 at 23:32
  • @MarcusLeon Yes I should have been more explicit. I use `[AppDelegate sharedDelegate].window.rootViewController = pointerToLoginVCInstantiatedViaStoryboard;` now when transitioning back to the login screen. – BFeher Sep 20 '16 at 02:42
  • @Honey I could be wrong, but in the OP and in my own modified version which I use in a few apps, we are not creating any new instances of `UIWindow` which need to then be made key and visible. Instead we are just replacing the `.rootViewController` of the already key and visible `self.window`. I'm open to suggestions and improvements to this current solution though. It seems that different version of iOS handle the solution differently in terms of smoothness and flicker. – BFeher Sep 20 '16 at 02:44
  • The top answer is not good. But this is also really bad practice to use `Notification` pattern in that way. – skywinder Aug 10 '17 at 16:36
  • i have noticed in this solution that the initial viewcontroller still gets loaded in the background (after the login screen was presented) – SoliQuiD Dec 10 '17 at 11:45
20

EDIT: Add logout action.

enter image description here

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.

Dimitris Bouzikas
  • 4,461
  • 3
  • 25
  • 33
  • 3
    sample project helped me very much to understand the concept of login n logout.. many thanks :) – Dave Jan 08 '15 at 12:04
17

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:

  1. 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).

  2. AppDelegate is responsible for getting login status updates.

  3. 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).

  4. ModalDismissAnimator and ModalPresentAnimator are used to customize default push animation.

How animators logic works:

  1. AppDelegate sets itself as a delegate of self.window.rootViewController (which is UINavigationController).

  2. AppDelegate returns one of animators in -[AppDelegate navigationController:animationControllerForOperation:fromViewController:toViewController:] if necessary.

  3. 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]];
         }];
    }
    

Test project is here.

Souhaib Guitouni
  • 860
  • 1
  • 7
  • 24
derpoliuk
  • 1,756
  • 2
  • 27
  • 43
  • 3
    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: http://stackoverflow.com/questions/8053832/rootviewcontroller-animation-transition-initial-orientation-is-wrong – HughHughTeotl Jul 10 '15 at 22:21
  • 2
    @HughHughTeotl Thank you for the comment and for the link. I updated my answer. – derpoliuk Jul 18 '15 at 11:42
  • 1
    @derpoliuk what if my base view controller is a UITabBarController? I can't push it in a UINavigationController. – Giorgio Jun 09 '17 at 13:52
  • @Giorgio, it's an interesting question, I didn't use `UITabBarController` for a very long time. I'd probably start with [window approach](http://irace.me/window) instead of manipulating view controllers. – derpoliuk Jun 23 '17 at 08:04
12

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!

pkamb
  • 33,281
  • 23
  • 160
  • 191
Harry Bloom
  • 2,359
  • 25
  • 17
  • 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 Oct 03 '17 at 09:24
  • @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. – shannoga Nov 14 '17 at 19:35
  • 1
    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. – Harry Bloom Apr 23 '18 at 11:58
  • @HarryBloom how would one use the `handleLogout` functionality? – nithinisreddy May 19 '20 at 21:37
  • 1
    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. – Harry Bloom Jun 05 '20 at 08:45
8

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.

Mihado
  • 1,487
  • 3
  • 17
  • 30
  • 2
    Should the view appear (be visible to the user) in `viewDidAppear'? This will still create a flicker. – Mark13426 Oct 07 '16 at 07:54
  • 3
    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. – skywinder Aug 10 '17 at 16:56
6

Create **LoginViewController** and **TabBarController**.

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!

iAleksandr
  • 595
  • 10
  • 11
  • 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? – ShadeToD May 24 '19 at 14:56
  • @iAleksandr Please update the answer for iOS 13. Coz of SceneDelegate current answer isn't working. – Nitesh Nov 23 '19 at 09:53
  • Hey Bro. Your Code isn't working when User Tapped on Sign Up. Please Add this Feature too.. – iFateh Jan 13 '22 at 06:29
5

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")
Mahbub Morshed
  • 915
  • 9
  • 20
3

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.

Thorsten
  • 3,102
  • 14
  • 14
1

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

  1. 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
  1. 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 }     
    }
    
Naresh
  • 16,698
  • 6
  • 112
  • 113
salanswift
  • 36
  • 3
0

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.

amb
  • 4,798
  • 6
  • 41
  • 68
0

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.

  1. Splash screen storyboard - for app initialisation and checking if the user is already logged in
  2. Login storyboard - for handling user login flow
  3. 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.

4b0
  • 21,981
  • 30
  • 95
  • 142
Jithin
  • 913
  • 5
  • 6
-1

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()
}
WangYang
  • 1
  • 2
  • 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. – Noel Widmer May 28 '17 at 16:19
  • 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. – WangYang May 29 '17 at 05:32
  • 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! :) – Noel Widmer May 29 '17 at 06:23
  • 1
    Tanks for your suggest.I have added the explanation.Thanks again.@Noel Widmer. – WangYang May 29 '17 at 10:18
  • Vague solution that does not highlight the use of 'Common' keyword. – Samarey Dec 27 '19 at 23:34
-3

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