108

I have a storyboard set up with working login and main view controller, the latter is the view controller to which the user is navigated to when login is successful. My objective is to show the main view controller immediately if the authentication (stored in keychain) is successful, and show the login view controller if the authentication failed. Basically, I want to do this in my AppDelegate:

// url request & response work fine, assume success is a BOOL here
// that indicates whether login was successful or not

if (success) {
          // 'push' main view controller
} else {
          // 'push' login view controller
}

I know about the method performSegueWithIdentifier: but this method is an instance method of UIViewController, so not callable from within AppDelegate. How do I do this using my existing storyboard ??

EDIT:

The Storyboard's initial view controller now is a navigation controller which isn't connected to anything. I used the setRootViewController: distinction because MainIdentifier is a UITabBarController. Then this is what my lines look like:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{        
    BOOL isLoggedIn = ...;    // got from server response

    NSString *segueId = isLoggedIn ? @"MainIdentifier" : @"LoginIdentifier";
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewController *initViewController = [storyboard instantiateViewControllerWithIdentifier:segueId];

    if (isLoggedIn) {
        [self.window setRootViewController:initViewController];
    } else {
        [(UINavigationController *)self.window.rootViewController pushViewController:initViewController animated:NO];
    }

    return YES;
}

Suggestions/improvements are welcome!

mmvie
  • 2,571
  • 7
  • 24
  • 39

10 Answers10

172

I'm surprised at some of the solutions being suggested here.

There's really no need for dummy navigation controllers in your storyboard, hiding views & firing segues on viewDidAppear: or any other hacks.

If you don't have the storyboard configured in your plist file, you must create both the window and the root view controller yourself :

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{        
    BOOL isLoggedIn = ...;    // from your server response

    NSString *storyboardId = isLoggedIn ? @"MainIdentifier" : @"LoginIdentifier";
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewController *initViewController = [storyboard instantiateViewControllerWithIdentifier:storyboardId];

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = initViewController;
    [self.window makeKeyAndVisible];

    return YES;
}

If the storyboard is configured in the app's plist, the window and root view controller will already be setup by the time application:didFinishLaunching: is called, and makeKeyAndVisible will be called on the window for you.

In that case, it's even simpler:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{        
    BOOL isLoggedIn = ...;    // from your server response

    NSString *storyboardId = isLoggedIn ? @"MainIdentifier" : @"LoginIdentifier";
    self.window.rootViewController = [self.window.rootViewController.storyboard instantiateViewControllerWithIdentifier:storyboardId];

    return YES;
}
followben
  • 9,067
  • 4
  • 40
  • 42
  • @AdamRabung Spot on - I'd just copied the OP's variable names, but I've updated my answer for clarity. Cheers. – followben Feb 13 '13 at 22:35
  • for the storyboard case: If you are using UINavigationViewcontroller as your root view controller then you'll need to push the next view controller. – Shirish Kumar Jul 12 '13 at 22:49
  • This is more intuitive way for me rather than go thru complex hierarchy navigation controller. Love this – Elliot Yap Feb 24 '14 at 01:38
  • Hello @followben, in my app, I have my rootViewController in storyBoard, its a tabBarController, and all the associated VCs with tabBar are also designed in VC, so now I have a case, where I want to show walkthrough of my app, So now when my app is first launched, I want to make walkthrough VC as the root VC instead of tabBarcontroller and when my walkthrough finishes , I want to make tabBarController as the rootViewController. How to do it I am not understanding – Ranjit Mar 25 '14 at 13:39
  • Excellent solution. But I prefer calling `presentViewController:` or `performSegueWithIdentifier:` from rootVC to preparing two separate storyboards. – Blaszard Aug 03 '15 at 10:34
  • To me this is clearly the best answer, I am wondering why it doesn't move up at least to the second place?! – lukas_o Aug 06 '15 at 08:40
  • This should be the accepted answer. Works perfectly. – emem Dec 16 '15 at 11:35
  • 1
    What if the request to the server is asynchronous? – Lior Burg Mar 10 '16 at 13:49
  • @LiorBurg then don't do the logged in check in `-application:didFinishLaunchingWithOptions:`. Instead, set the initial viewController in your storyboard to something sensible (e.g. a VC that looks like your launch storyboard, but with a UIActivityIndicatorView in the middle), and do the automatic login in there. On success or failure, call `-performSegueWithIdentifier:` to present either the main view or the login view. – followben Mar 10 '16 at 21:51
25

I assume your storyboard is set as the "main storyboard" (key UIMainStoryboardFile in your Info.plist). In that case, UIKit will load the storyboard and set its initial view controller as your window's root view controller before it sends application:didFinishLaunchingWithOptions: to your AppDelegate.

I also assume that the initial view controller in your storyboard is the navigation controller, onto which you want to push your main or login view controller.

You can ask your window for its root view controller, and send the performSegueWithIdentifier:sender: message to it:

NSString *segueId = success ? @"pushMain" : @"pushLogin";
[self.window.rootViewController performSegueWithIdentifier:segueId sender:self];
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    I implemented your lines of code in my application:didFinishLaunchingWithOptions: method. Debugging shows the rootViewController is indeed the initial navigation controller, the segue however is not performed (navigation bar is showing, rest is black). I must say the initial navigation controller does not have a rootViewController any longer, only 2 segues (StartLoginSegue and StartMainSegue). – mmvie Dec 11 '11 at 11:43
  • 3
    Yep, not working for me either. Why did you mark it Answered if it doesnt work for you? – daihovey Jun 15 '12 at 03:19
  • 3
    I believe this is the correct answer. You need to 1. have a window property on your app delegate and 2. call `[[self window] makeKeyAndVisible]` in application:didFinishLaunchingWithOptions: before you try and perform the conditional segues. UIApplicationMain() is suppose to message makeKeyAndVisible but does so only after didFinish...Options: finishes. Look for "Coordinating Efforts Between View Controllers" in Apple docs for details. – edelaney05 Jun 28 '12 at 14:12
  • This is the right idea, but doesn't quite work. See my answer for a working solution. – Matthew Frederick Jul 10 '12 at 20:44
  • @MatthewFrederick Your solution will work if the initial controller is a navigation controller, but not if it's a plain view controller. The real answer is just to create the window and root view controller yourself - indeed this is what Apple recommends in the View Controller Programming Guide. See my answer below for details. – followben Oct 09 '12 at 12:00
18

IF your storyboard's entry point isn't an UINavigationController:

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


    //Your View Controller Identifiers defined in Interface Builder
    NSString *firstViewControllerIdentifier  = @"LoginViewController";
    NSString *secondViewControllerIdentifier = @"MainMenuViewController";

    //check if the key exists and its value
    BOOL appHasLaunchedOnce = [[NSUserDefaults standardUserDefaults] boolForKey:@"appHasLaunchedOnce"];

    //if the key doesn't exist or its value is NO
    if (!appHasLaunchedOnce) {
        //set its value to YES
        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"appHasLaunchedOnce"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    //check which view controller identifier should be used
    NSString *viewControllerIdentifier = appHasLaunchedOnce ? secondViewControllerIdentifier : firstViewControllerIdentifier;

    //IF THE STORYBOARD EXISTS IN YOUR INFO.PLIST FILE AND YOU USE A SINGLE STORYBOARD
    UIStoryboard *storyboard = self.window.rootViewController.storyboard;

    //IF THE STORYBOARD DOESN'T EXIST IN YOUR INFO.PLIST FILE OR IF YOU USE MULTIPLE STORYBOARDS
    //UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"YOUR_STORYBOARD_FILE_NAME" bundle:nil];

    //instantiate the view controller
    UIViewController *presentedViewController = [storyboard instantiateViewControllerWithIdentifier:viewControllerIdentifier];

    //IF YOU DON'T USE A NAVIGATION CONTROLLER:
    [self.window setRootViewController:presentedViewController];

    return YES;
}

IF your storyboard's entry point IS an UINavigationController replace:

//IF YOU DON'T USE A NAVIGATION CONTROLLER:
[self.window setRootViewController:presentedViewController];

with:

//IF YOU USE A NAVIGATION CONTROLLER AS THE ENTRY POINT IN YOUR STORYBOARD:
UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
[navController pushViewController:presentedViewController animated:NO];
Razvan
  • 4,122
  • 2
  • 26
  • 44
  • 1
    Worked well. Just a comment, doesn't this show "firstViewControllerIdentifier" only after they have entered initially? So shouldn't it be reversed? `appHasLaunchedOnce ? secondViewControllerIdentifier : firstViewControllerIdentifier;` – ammianus Aug 03 '17 at 19:58
  • @ammianus you're correct. They should be reversed and I edited. – Razvan Aug 08 '17 at 14:59
10

In your AppDelegate's application:didFinishLaunchingWithOptions method, before the return YES line, add:

UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
YourStartingViewController *yourStartingViewController = [[navigationController viewControllers] objectAtIndex:0];
[yourStartingViewController performSegueWithIdentifier:@"YourSegueIdentifier" sender:self];

Replace YourStartingViewController with the name of your actual first view controller class (the one you don't want to necessarily appear) and YourSegueIdentifier with the actual name of the segue between that starting controller and the one you want to actually start on (the one after the segue).

Wrap that code in an if conditional if you don't always want it to happen.

Matthew Frederick
  • 22,245
  • 10
  • 71
  • 97
6

Given that you're already using a Storyboard, you can use this to present the user with MyViewController, a custom controller (Boiling down followben's answer a bit).

In AppDelegate.m:

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    MyCustomViewController *controller = [self.window.rootViewController.storyboard instantiateViewControllerWithIdentifier:@"MyCustomViewController"];

    // now configure the controller with a model, etc.

    self.window.rootViewController = controller;

    return YES;
}

The string passed to instantiateViewControllerWithIdentifier refers to the Storyboard ID, which can be set in interface builder:

enter image description here

Just wrap this in logic as needed.

If you're starting with a UINavigationController, though, this approach will not give you navigation controls.

To 'jump forward' from the starting point of a navigation controller set up through interface builder, use this approach:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UINavigationController *navigation = (UINavigationController *) self.window.rootViewController;

    [navigation.visibleViewController performSegueWithIdentifier:@"my-named-segue" sender:nil];

    return YES;
}
Community
  • 1
  • 1
Rich Apodaca
  • 28,316
  • 16
  • 103
  • 129
4

Why not have the login screen which appears first, check if the user is already logged in and push the next screen straight away? All in the ViewDidLoad.

Darren
  • 10,182
  • 20
  • 95
  • 162
  • 2
    This works indeed, but my goals is to show the launch image as long as the app is still waiting for the server response (whether the login was successful or not). Just like the Facebook app... – mmvie Dec 12 '11 at 23:56
  • 2
    You could always have your first view just a UIImage that uses the same image as your splash and in the background check to see if logged in and display the next view. – Darren Dec 13 '11 at 10:18
3

Swift implementation of same :

If you use UINavigationController as entry point in storyboard

let storyboard = UIStoryboard(name: "Main", bundle: nil)

var rootViewController = self.window!.rootViewController as! UINavigationController;

    if(loginCondition == true){

         let profileController = storyboard.instantiateViewControllerWithIdentifier("ProfileController") as? ProfileController  
         rootViewController.pushViewController(profileController!, animated: true) 
    }
    else {

         let loginController =   storyboard.instantiateViewControllerWithIdentifier("LoginController") as? LoginController 
         rootViewController.pushViewController(loginController!, animated: true) 
    }
Dashrath
  • 2,141
  • 1
  • 28
  • 33
1

This is the solution that worked n iOS7. To speed up the initial loading and not do any unnecessary loading, I have a completely empty UIViewcontroller called "DUMMY" in my Storyboard file. Then I can use the following code:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UIStoryboard* storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];

    NSString* controllerId = @"Publications";
    if (![NSUserDefaults.standardUserDefaults boolForKey:@"hasSeenIntroduction"])
    {
        controllerId = @"Introduction";
    }
    else if (![NSUserDefaults.standardUserDefaults boolForKey:@"hasDonePersonalizationOrLogin"])
    {
        controllerId = @"PersonalizeIntro";
    }

    if ([AppDelegate isLuc])
    {
        controllerId = @"LoginStart";
    }

    if ([AppDelegate isBart] || [AppDelegate isBartiPhone4])
    {
        controllerId = @"Publications";
    }

    UIViewController* controller = [storyboard instantiateViewControllerWithIdentifier:controllerId];
    self.window.rootViewController = controller;

    return YES;
}
Luc Bloom
  • 1,120
  • 12
  • 18
0

I suggest to create a new MainViewController that is Root View Controller of Navigation Controller. To do that, just hold control, then drag connection between Navigation Controller and MainViewController, choose 'Relationship - Root View Controller' from prompt.

In MainViewController:

- (void)viewDidLoad
{
    [super viewDidLoad];
    if (isLoggedIn) {
        [self performSegueWithIdentifier:@"HomeSegue" sender:nil];
    } else {
        [self performSegueWithIdentifier:@"LoginSegue" sender:nil];
    }
}

Remember to create segues between MainViewController with Home and Login view controllers. Hope this helps. :)

thanhbinh84
  • 17,876
  • 6
  • 62
  • 69
0

After trying many different methods, I was able to solve this problem with this:

-(void)viewWillAppear:(BOOL)animated {

    // Check if user is already logged in
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    if ([[prefs objectForKey:@"log"] intValue] == 1) {
        self.view.hidden = YES;
    }
}

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

    // Check if user is already logged in
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    if ([[prefs objectForKey:@"log"] intValue] == 1) {
        [self performSegueWithIdentifier:@"homeSeg3" sender:self];
    }
}

-(void)viewDidUnload {
    self.view.hidden = NO;
}
AddisDev
  • 1,791
  • 21
  • 33
  • If you're not too far along Taylor, you might want to refactor to something simpler. See my answer for details :) – followben Oct 09 '12 at 12:01