5
votes

I have an iOS app that has a connection to a server. If we get disconnected, I want to be able to dismiss the top view controllers to get back to a "connecting to server" view controller. The problem is that a disconnection can occur at any time, including during a transition between view controllers.

The view controller hierarchy is like so:

  1. ConnectingToServerViewController
  2. SignInViewController
  3. MainAppViewController
  4. Other view controllers

When a disconnection is detected I want the view hierarchy to collapse back to:

  1. ConnectingToServerViewController

So when a disconnection is detected, this method is called on the ConnectingToServerViewController to dismiss anything that it has presented and go back to attempting to connect to server:

- (void)restartSession
{
    if (self.presentedViewController) {
        [self dismissViewControllerAnimated:NO completion:nil];
    }
}

However, if I try to dismiss while a view transition is occurring, I get errors such as

*** Assertion failure in -[UIWindowController transition:fromViewController:toViewController:target:didEndSelector:], /SourceCache/UIKit/UIKit-2380.17/UIWindowController.m:211

attempt to dismiss modal view controller whose view does not currently appear. self = <YYYYYViewController: 0x2089c8a0> modalViewController = <XXXXXViewController: 0x208e6610>
attempt to dismiss modal view controller whose view does not currently appear. self = <WWWWWWViewController: 0x1fd9e990> modalViewController = <YYYYYViewController: 0x2089c8a0>

The first of which will crash the app, the second will just not dismiss anything and continue to show the current presented view controller.

Thoughts:

  1. delays won't work since we don't know when to start the delay
  2. is there a way to track when view transitions complete?
  3. should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?
  4. perhaps instead of dismiss, I should just set a new root view controller?
  5. I've made sure that all overridden view(will|did)(dis)?appear methods call the appropriate super method.
  6. Any solution that requires all view controllers to override view(did|will)appear methods to track state sounds like it could cause issues if we forget to set the base class for a new view controller.
4
if you can set any flag variable in viewController and when you are disconnected from server then if view is not loaded then you can change flag from restrtSesssion method. so that when view loads it will automatically dismiss it self. you can use some logic like that... hope it will help.. - Armaan Stranger
I kind of touched upon this near the end. I wanted to avoid solutions that require all view controllers to have to subclass a special viewcontroller class just to keep track of state. - Nick Sonneveld
What about dismissing it with a delay or on the main queue by placing the dismissal inside: NSOperationQueue.mainQueue().addOperationWithBlock { // dismiss } // swift code - Erik Engheim

4 Answers

0
votes

Do something like this. Try this out once,

UIViewController *controller = self.presentingViewController; //THIS LINE IS IMP
[self dismissViewControllerAnimated:YES
                                 completion:^{
                                     [controller presentViewController:adminViewController animated:YES completion:nil];
                                     adminViewController.view.superview.frame    = CGRectMake(1024/2 - 400, 768/2 - 280, 800 , 560);//it's important to do this after
                                     [adminViewController release];
                                 }]; 
0
votes

One way that has worked for me is to assign a new view controller to the root view controller. That way, views in the old hierarchy can animate and transition to their hearts content while we have new controllers.

eg

- (void)restartSession
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    ConnectingToServerViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"ConnectingToServerViewController"];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [UIApplication sharedApplication].delegate.window.rootViewController = vc;
}

I'm not sure if I'm aware of all the downsides to this though. Perhaps the old view controllers will never get freed because of a dangling strong reference? We're no longer reusing ConnectingToServerViewController, we have to recreate that each time.

I based the code on what I saw in this answer for Managing and dismissing Multiple View Controllers in iOS.

0
votes

It seems like you are trying to dismiss the view controller when it is not currently on screen. To check if it is on screen you could use:

if (self.presentedViewController.view.window) 
{
    [self dismissViewControllerAnimated:NO completion:nil];
}
else 
{
    self.presentedViewController = nil;
}
0
votes

I will answer in order.

is there a way to track when view transitions complete?

You could try with the UINavigationControllerDelegate (if you are using one of those). Other approach could be using a custom animator.

should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?

That's an option. You are free to do it if you want. Another option is not to do that. I think that container view controllers such as navigation controller has better approaches.

I should just set a new root view controller?

I would suggest to do the opposite. I would set the SignInViewController / MainAppViewController as the root flow, and present modally ConnectingToServerViewController on demand. In my opinion that's a healthier approach.

Hope it helps.