15

I've just started on iOS programming and so far the tutorials and answers I found here have been a great help to move forward. However, this particular problem has been bumming me all night and I can't find an answer that "feels right".

I'm writing an application that connects to a remote service and the users need to sign in before they can use it. When they start using the application, their first view should be the sign in dialog; when they've authenticated before, they immediately see the overview page.

The project uses story boards - which I think is a great feature - so most of the code that selects and loads the root view controller is already taken care of. I thought the best place to add my logic is the application:didFinishLaunchingWithOptions: method of the AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
      (NSDictionary *)launchOptions
{
    // select my root view controller here based on credentials present or not
    return YES;
}

But this brought up two questions:

  1. Inside this particular delegate method, the root view controller has already been selected (and loaded?) based on the story board. Could I move to an earlier spot in the loading process to override the first view controller selection or would that needlessly complicate matters?

  2. To override the first view controller I need a reference to the story board, but I couldn't find a better way than to use the storyboardWithName:bundle: constructor of UIStoryboard. That feels wrong, the application should already have a reference to the story board, but how can I access it?

Update

I worked out the second issue I was having, as I found my answer here:

UIStoryboard: What's the Correct Way to Get the Active Storyboard?

NSBundle *bundle = [NSBundle mainBundle];
NSString *sbFile = [bundle objectForInfoDictionaryKey:@"UIMainStoryboardFile"];
UIStoryboard *sb = [UIStoryboard storyboardWithName:sbFile bundle:bundle];

The above will create a new story board instance; to get the active instance, it's a whole lot simpler:

UIStoryboard *sb = [[self.window rootViewController] storyboard];

In the story board file itself you have to set an identifier for the view you wish to load, e.g. LoginDialog. Afterwards you instantiate the view like this:

LoginViewController *login = [sb instantiateViewControllerWithIdentifier:@"LoginDialog"];
[self.window setRootViewController:login];

Within another view controller, the following suffices:

UIStoryboard *sb = self.storyboard;
LoginViewController *login = [sb instantiateViewControllerWithIdentifier:@"LoginDialog"];
[self presentViewController:login animated:NO completion:nil];
Community
  • 1
  • 1
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309

4 Answers4

13

You can just reset the root view controller of the window

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
      (NSDictionary *)launchOptions
{
   if(your_condition) {
       UIViewController *newRoot = [your implementation];
       self.window.rootViewController = newRoot;
   }
   return YES;
}

This is worked for me, Xcode5.0.1

Danyun Liu
  • 3,072
  • 24
  • 22
  • 5
    `/* For storyboards... */ self.window.rootViewController = (YourViewController *)[[UIStoryboard storyboardWithName:@"Main" bundle: nil] instantiateViewControllerWithIdentifier:@"YourViewControllerID"];` – John Erck Jan 09 '14 at 17:54
7

I have a similar scenario as yours. My application uses a UINavigationController as the root view controller. If the user is logged in, I want to present him/her with NotLoggedInViewController, while if it's logged in I want to show the LoggedInViewController.

In a storyboard a UINavigationController can only have one child, so you have to be able to programmatically assign another root view controller to it.

I start by creating a custom navigation controller class, let's name it MyNavigationController. In the storyboard I assign this custom class to the navigation controller object.

Still in the storyboard, I then model both view controllers, and connect one of them to the navigation controller object. Since I need to be able to access them from my code later on, I assign each of them an identifier using the XCode inspector on the right. These identifiers which can be arbitrary strings, but to things simple I just use the class names.

Finally I then implement the viewDidLoad method on MyNavigationController class:

BOOL isLoggedIn = ...;

- (void)viewDidLoad {
  id rootController;
  if (isLoggedIn) {
    rootController = [self.storyboard instantiateViewControllerWithIdentifier:@"LoggedInViewController"];
  } else {
    rootController = [self.storyboard instantiateViewControllerWithIdentifier:@"NotLoggedInViewController"];
  }
  self.viewControllers = [NSArray arrayWithObjects:rootController, nil];
}
ovidiu
  • 1,129
  • 1
  • 10
  • 10
  • Thanks for the answer; it confirms the answer I found myself regarding how to get a reference to the active storyboard and instantiate the view from it :) my decision is moving towards adding the logic inside the app delegate, mainly to handle a password change centrally (the remote service does not support sessions and the hashed password is sent at every request) ... still working on that – Ja͢ck Jun 02 '12 at 08:57
2

I had hardly used storyboard & probably this is not the exact answer to your question. But I will suggest you some way what I did in my project created without using a storyboard.

In didFinishLaunchingWithOptions AuthenticationViewController is the first view loaded. It asks login credentials. Once entered it will enter the actual ViewControllers(viz. TabBar &all..) used by project.

Interesting feature added to project is, when you enter credentials I popped up an UIAleretView that asks user to choose one of the three options.

  1. Save Credentials without passcode
  2. Save Credentials with passcode
  3. Dont save credentials

Here pass code is nothing but 4digit number entered by user. Whenever he wants to 'Save Credentials with passcode', I pushViewController that shows NumberPad instad of default keyboard & popviewController when it finishes entering of pin. If user 'Dont save credentials' & later on while playing the app wants to go for other authentication options then I added the last tab of TabBarController as 'Settings' tab inside which I allow user to choose one of the Authentication options, popped as UIAlertView in the beginning of app start after login.

Dont forget to Save credentials in keychain

In a nutshell,

  1. AuthenticationViewController-> check if login credentials are stored in keychain

1.1. If not stored(i.e. 3. Dont save credentials)-> then show Login page.

1.2. If credentials are saved in keychain-> extract them & see if it is tied with passcode.

1.2.1. If it is tied with passcode(i.e. 2. Save Credentials with passcode )-> then show passcode page.

1.2.2. If it is not tied (1. Save Credentials without passcode)-> then show/load you project's TabBarController hierarchy or other stuff. here actually your app start.

hp iOS Coder
  • 3,230
  • 2
  • 23
  • 39
  • 1
    Thanks! I like the addition of a pass code; currently I just write the account details in the key chain. Of course, I could have the authentication view perform the view change based on whether I could find the stored credentials, but I'd rather have it done earlier on. – Ja͢ck May 31 '12 at 04:19
  • Ok. When you succeed in doing this using storyboard, then I would suggest you to post this as a sample project somewhere on github or your blog. This will really guide beginners alot. – hp iOS Coder May 31 '12 at 05:06
  • 1
    Sure! Found out the second part of the question btw :) – Ja͢ck May 31 '12 at 06:46
-1

With the main storyboard already loaded, it's just a matter of finding its reference so that I can instantiate another root view controller:

UIStoryboard *mainStoryboard = self.window.rootViewController.storyboard;

self.window.rootViewController = [mainStoryboard 
    instantiateViewControllerWithIdentifier:@"view-controller-id"];
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • It will blow up. Segues will be broken, you'll get some warnings from UIKit, and god knows, maybe in iOS 9 that will lead to CRASH. – pronebird Sep 19 '14 at 12:52
  • @Andy Out of curiosity, what are the warnings from UIKit? – Ja͢ck Sep 19 '14 at 12:57
  • "Warning: Attempt to present XXX on XXX whose view is not in the window hierarchy!" But I see it's gone after cleaning state restoration cache. In any case, this approach breaks storyboard flow and state restoration flow. – pronebird Sep 19 '14 at 13:15
  • @Andy Fair enough; so what would the recommended approach be? My project currently has a root view controller that performs a segue to the main app if a user is signed in ... that works :) – Ja͢ck Sep 19 '14 at 17:13
  • I use navigation controller and simply push next controller if user signed in already. The good thing you can do that without animation, unlike performSegue approach. Next controller hides back button so user can't get to sign in. You can also do performSegue in viewDidAppear of root VC, but that will reveal root VC for a moment, which might not be desired effect. – pronebird Sep 19 '14 at 17:40