18

In Xcode 5.0.2 I've created an iPhone app (the complete app code is on GitHub, please run pod install once - for SDWebImage), which displays a list of social networks (Facebook, Google+, etc.), then - in another view - an UIWebView displaying OAuth2 web page and then a third view with user avatar and details (first name, last name, etc.):

Xcode screenshot

(Here the fullscreen screenshot of the storyboard shown above)

enter image description here

Before I display the user avatar and details in the last view, I save the user data to NSUserDefaults.

My question is: when the user starts the app the next time and there is user data in NSUserDefaults already - how can I skip the first 2 views and display (without any animation) the last view with those user details?

I've put the following code to the AppDelegate.m:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [defaults objectForKey:@"key"];
    User *user = [User loadForKey:key];

    if (user) {
        UINavigationController *nc = (UINavigationController*)self.window.rootViewController;
        // XXX how to load the last view here? XXX
    }

    return YES;
}

and can see, that the user data is being loaded okay, but I don't understand how to jump to the last view (esp. because most of the UI is defined in the storyboard)?

UPDATE: I have tried following Tommy Alexander's suggestion (thanks!) with the following code in AppDelegate.m (and also assigned Details StoryboardID to the last view):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [defaults objectForKey:@"key"];
    User *user = [User loadForKey:key];

    if (user) {
        UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        UIViewController *uvc = [storyboard instantiateViewControllerWithIdentifier:@"Details"];
        NSLog(@"XXX uvc=%@ user=%@", uvc, user);
        [self.window.rootViewController presentViewController:uvc animated:YES completion:nil];
    }

    return YES;
}

and while the last view (the UserViewController with user avatar and details) seems to be instantiated okay, I get the following warning and the app doesn't jump to the last view:

MyAuth[3917:70b] XXX uvc=<UserViewController: 0x8acbac0> user=
VK
59751265
Alexander
Farber
1945522
http://cs319319.vk.me/v319319265/b7df/TnyKeffL_mU.jpg
0
MyAuth[3917:70b] Warning: Attempt to present <UserViewController: 0x8acbac0> on <UINavigationController: 0x8bb3a30> whose view is not in the window hierarchy!

UPDATE 2: When I move the above code to the viewDidLoad of the 1st view (the MasterViewController.m, where the user can select a social network), then I get another warning:

MyAuth[1358:70b] XXX uvc=<UserViewController: 0x8a97430> user=
FB
597287941
Alexander
Farber
Bochum, Germany
http://graph.facebook.com/597287941/picture?type=large
0
MyAuth[1358:70b] Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x8a893e0>.

UPDATE 3: Thanks for the valuable answers! I've ended up using Leonty Deriglazov suggestion:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *vc1 = [storyboard instantiateViewControllerWithIdentifier:@"Master"];
    //UIViewController *vc2 = [storyboard instantiateViewControllerWithIdentifier:@"Login"];
    UIViewController *vc3 = [storyboard instantiateViewControllerWithIdentifier:@"User"];

    User *user = [User load];
    NSArray *controllers = (user ? @[vc1, vc3] : @[vc1]);

    UINavigationController *nc = (UINavigationController *)self.window.rootViewController;
    [nc setViewControllers:controllers];

    return YES;
}
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • 4
    I guess you could add another segue from view 1 to view 4 and trigger i – Jack Feb 03 '14 at 21:08
  • Should I ctrl-drag the segue from the navigation controller to the last view? – Alexander Farber Feb 04 '14 at 11:48
  • I don't know.. maybe try this UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; UIViewController *uvc = [storyboard instantiateViewControllerWithIdentifier:@"Details"]; [self.window setRootViewController:uvc]; if not then make UINavigationController as rootViewController and make ivc as its rootViewController.. Hope that helps – Taseen Feb 04 '14 at 23:32
  • See Update in my answer – Taseen Feb 04 '14 at 23:38
  • @AlexanderFarber - I have posted an answer which always worked for me – bhavya kothari Feb 06 '14 at 12:35
  • 1
    @AlexanderFarber I've edited my answer to include info about your "UPDATE 2" problems. – nikolovski Feb 09 '14 at 12:21

10 Answers10

15

In a nutshell, the most straightforward solution is to use -[UINavigationController setViewControllers:animated:].

You need to do the following to achieve what you want:

  1. assign storyboard IDs to all your controllers,
  2. instantiate all three view controller from the story board (this is lightweight as long as you don't do most of the initialisation work in controller's constructors)
  3. put them into an array in the same order you have them in storyboard,
  4. call -[UINavigationController setViewControllers:] with the array as the first argument.

That's it. The code in your -[AppDelegate application:didFinishLaunchingWithOptions:] might look like this (after checking your skipping condition of course):

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *vc1 = [storyboard instantiateViewControllerWithIdentifier:@"MyAuth"]; //if you assigned this ID is storyboard
UIViewController *vc2 = [storyboard instantiateViewControllerWithIdentifier:@"Login"];  //if you assigned this ID is storyboard
UIViewController *vc3 = [storyboard instantiateViewControllerWithIdentifier:@"Details"];
NSArray *controllers = @[vc1, vc2, vc3];
UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
[navController setViewControllers:controllers];

If you paste just this to -[AppDelegate application:didFinishLaunchingWithOptions:], you'll see that it works immediately.

Leon Deriglazov
  • 1,132
  • 9
  • 13
  • Thanks, this seems to be in fact the most straightforward and quick solution: I don't even have to load a VC, before I have to segue/present another VC. – Alexander Farber Feb 10 '14 at 08:24
  • 1
    @AlexanderFarber Cheers! ;-) Feel free to reach out to me if you have any more questions — I'd be glad to help. ;-) – Leon Deriglazov Feb 10 '14 at 09:39
4

Breaking view hierarchy will always gives weird behavior.

By using below mentioned approach user will not be able to notice that LoginViewController is presented and then we have pushed to Detail screen.

This approach always worked for me

In LoginViewController.m File

- (void)viewDidLoad
{

if (user) {
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UserViewController *userViewController = [storyboard instantiateViewControllerWithIdentifier:@"Details"];


[self.navigationController pushViewController:listViewVC animated:YES];

    }
}

In UserViewController.m File

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

if (user) {

    self.navigationItem.hidesBackButton = YES;

}

}
bhavya kothari
  • 7,484
  • 4
  • 27
  • 53
  • This solutions spreads the knowledge of one particular navigation behaviour (skipping 2 views) across multiple controllers. Wouldn't it be getter to consolidate the knowledge of this feature in one place? It would be easier to write and easier to test/debug. – Leon Deriglazov Feb 07 '14 at 19:30
3

I think your problem starts with your initial Storyboard setup. I would use something like the below image.

I would make the User View controller my root view controller and present the login view controllers modally with or without animation.

Once the login succeeds it will be pretty convenient to dismiss the view controllers. And if the user logs out later on you'll just need to present the login view controllers again.

enter image description here

Desdenova
  • 5,326
  • 8
  • 37
  • 45
  • Thanks, but it seems to me, that the order of VCs is just a minor detail (like if you'll see short flashing of the table view on app startup or not). With my question I'm after **source code**, allowing me to push 2 VCs and jump to a 3rd one (whatever that will be). – Alexander Farber Feb 06 '14 at 13:57
3

I think you can do this :

if (hasUser) {
    MyFirstViewController *firstViewController = [myStoryboard instantiateViewControllerWithIdentifier:@"FirstViewControllerStoryboardId"];
    SecondViewController *secondViewController = [myStoryboard instantiateViewControllerWithIdentifier:@"SecondViewControllerStoryboardId"];

    [myNavigationController pushViewController:firstViewController animated:NO];
    [myNavigationController pushViewController:secondViewController animated:YES];
}

This way you will not have any animations and the navigation stack will be respected. Change the 'animated' boolean to play with the animation(s).

Hope it helps !

Rom.
  • 2,800
  • 2
  • 17
  • 19
2

According to your screen workflow, you have a navigation controller and other view controllers are pushed into it.

So, the logic is simple: if a user is cached set details view controller as a root of your navigation view controller, else do nothing (storyboard's default will be used).

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

    if (hasUser) {
        UIViewController *vc = [nc.storyboard instantiateViewControllerWithIdentifier:@"DetailsViewController"];
        nc.viewControllers = @[vc];
    }

    return YES;
}

UPDATE

In case you need to go back to previous view controllers (e.g. from details to table), you can set the whole navigation hierarchy in navigation controller:

UINavigationController *nc = (UINavigationController*)self.window.rootViewController;
if (hasUser) {
    UIViewController *vc = [nc.storyboard instantiateViewControllerWithIdentifier:@"DetailsViewController"];
    [nc pushViewController:vc animated:NO];
}
vokilam
  • 10,153
  • 3
  • 45
  • 56
  • Thanks, but then the user can't "Logout" to return to the TableView with the selection of social networks – Alexander Farber Feb 06 '14 at 10:22
  • 1
    Use the same technique and replace root of navigation controller with TableViewController in logout handler: `self.navigationController.viewControllers = @[tableViewController];` – vokilam Feb 06 '14 at 10:31
  • I still have the minor problem then that the "Back" button isn't shown – Alexander Farber Feb 06 '14 at 11:07
  • 1
    In that case restore desired hierarchy by using `nc.viewControllers = @[tableViewController, detailsViewController]` or pushing view controllers to navigation stack. See updated answer – vokilam Feb 06 '14 at 13:04
1

Try giving your last UIViewController a StoryBoardID in the Attributes Inspector in IB. Then, once you determine if you have a user in NSUSerDefault:

-Get a reference to your storyboard:

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

-Insatiate an instance of your last view controller (remember to import your last view contorllers .h file):

UIViewController *myController = [storyboard instantiateViewControllerWithIdentifier:@"StoryBoardID"];

-Present the VC:

[self presentViewController:myController animated:YES completion:nil]; 
Pradhyuman sinh
  • 3,936
  • 1
  • 23
  • 38
Tommy Alexander
  • 701
  • 1
  • 7
  • 17
  • Unfortunately, I get the `Warning: Attempt to present on whose view is not in the window hierarchy!` and the app doesn't skip to the last view - please see my updated question – Alexander Farber Feb 04 '14 at 11:36
1

Use this :

[self.presentingViewController dismissViewControllerAnimated:NO completion:^{
    [self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
}];

The best approach will be to use to UnwindSegue..

What are Unwind segues for and how do you use them?

UPDATE IF COMMENT IS NOT CLEAR

I don't know..

maybe try this

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *uvc = [storyboard instantiateViewControllerWithIdentifier:@"Details"];
[self.window setRootViewController:uvc];

if not then make UINavigationController as rootViewController and make ivc as its rootViewController..

Hope that helps

Community
  • 1
  • 1
Taseen
  • 1,213
  • 1
  • 11
  • 18
  • Thanks, but I don't understand, what to dismiss or unwind in my app? I am trying to skip the first 2 views and jump to the last view in the screenshot above... – Alexander Farber Feb 04 '14 at 12:20
1

What I do in one of my apps, is I have an additional segue from the rootViewController to the screen I want to skip to (in your example, from MyAuth to Details). Then, when I show the UINavigationController, I also set a variable in the rootViewController specifying if I want to skip to another screen. Finally, in the viewDidLoad: of the view controller I do a check:

// Check if we want to skip to another screen
if (self.skipToDetails == YES) {
    [self performSegueWithIdentifier:@"detailsView" sender:nil];
}

If you turn off animation for this segue in the storyboard file, the user won't see the first screen at all.

Also, you'll need to set hidesBackButton to YES in the Details screen.

Edit: The "unbalanced calls to begin/end" happens when you move to another VC (either push or modal) from the original (rootViewController) VC in viewDidLoad:. Moving the code to viewDidAppear: should help.

nikolovski
  • 4,049
  • 1
  • 31
  • 37
1

@Alexander please check screenshot you can manage the controller like this:

enter image description here

And use following code in user view controller:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [defaults objectForKey:@"key"];
User *user = [User loadForKey:key];

if (!user) {
 MasterViewController *loginController = [self.storyboard instantiateViewControllerWithIdentifier:@"MasterViewController"];
[self presentViewController:masterController animated:YES completion:nil];
}
Shubham
  • 570
  • 3
  • 12
1
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [defaults objectForKey:@"key"];
    User *user = [User loadForKey:key];

  UINavigationController *navcon = (UINavigationController *)self.window.rootViewController;

    if (hasUser) {
        UIViewController *viewcon = [navcon.storyboard instantiateViewControllerWithIdentifier:@"DetailsViewController"];
        [navcon pushViewController:viewcon animated:NO]; }   
}
else
   {
       UIViewController *viewcon = [navcon.storyboard instantiateViewControllerWithIdentifier:@"UserViewController"];
       [navcon pushViewController:viewcon animated:NO];}
}
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
Lineesh K Mohan
  • 1,702
  • 14
  • 18