modal View controllers - how to display and dismiss

Hema picture Hema · Feb 16, 2013 · Viewed 167.3k times · Source

I'm breaking my head for the last one week on how to solve the issue with showing and dismissing multiple view controllers. I have created a sample project and pasting the code directly from the project. I have 3 view controllers with their corresponding .xib files. MainViewController, VC1 and VC2. I have two buttons on the main view controller.

- (IBAction)VC1Pressed:(UIButton *)sender
{
    VC1 *vc1 = [[VC1 alloc] initWithNibName:@"VC1" bundle:nil];
    [vc1 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc1 animated:YES completion:nil];
}

This opens VC1 with no issues. In VC1, I have another button that should open VC2 while at the same time dismiss VC1.

- (IBAction)buttonPressedFromVC1:(UIButton *)sender
{
    VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];
    [vc2 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc2 animated:YES completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
} // This shows a warning: Attempt to dismiss from view controller <VC1: 0x715e460> while a presentation or dismiss is in progress!


- (IBAction)buttonPressedFromVC2:(UIButton *)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
} // This is going back to VC1. 

I want it go back to the main view controller while at the same time VC1 should have been removed from memory for good. VC1 should only show up when I click on the VC1 button on the main controller.

The other button on the Main view controller should also be able to display VC2 directly bypassing VC1 and should come back to the main controller when a button is clicked on VC2. There is no long running code, loops or any timers. Just bare bone calls to view controllers.

Answer

foundry picture foundry · Feb 16, 2013

This line:

[self dismissViewControllerAnimated:YES completion:nil];

isn't sending a message to itself, it's actually sending a message to its presenting VC, asking it to do the dismissing. When you present a VC, you create a relationship between the presenting VC and the presented one. So you should not destroy the presenting VC while it is presenting (the presented VC can't send that dismiss message back…). As you're not really taking account of it you are leaving the app in a confused state. See my answer Dismissing a Presented View Controller in which I recommend this method is more clearly written:

[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

In your case, you need to ensure that all of the controlling is done in mainVC . You should use a delegate to send the correct message back to MainViewController from ViewController1, so that mainVC can dismiss VC1 and then present VC2.

In VC2 VC1 add a protocol in your .h file above the @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

and lower down in the same file in the @interface section declare a property to hold the delegate pointer:

@property (nonatomic,weak) id <ViewController1Protocol> delegate;

In the VC1 .m file, the dismiss button method should call the delegate method

- (IBAction)buttonPressedFromVC1:(UIButton *)sender {
    [self.delegate dissmissAndPresentVC2]
}

Now in mainVC, set it as VC1's delegate when creating VC1:

- (IBAction)present1:(id)sender {
    ViewController1* vc = [[ViewController1 alloc] initWithNibName:@"ViewController1" bundle:nil];
    vc.delegate = self;
    [self present:vc];
}

and implement the delegate method:

- (void)dismissAndPresent2 {
    [self dismissViewControllerAnimated:NO completion:^{
        [self present2:nil];
    }];
}

present2: can be the same method as your VC2Pressed: button IBAction method. Note that it is called from the completion block to ensure that VC2 is not presented until VC1 is fully dismissed.

You are now moving from VC1->VCMain->VC2 so you will probably want only one of the transitions to be animated.

update

In your comments you express surprise at the complexity required to achieve a seemingly simple thing. I assure you, this delegation pattern is so central to much of Objective-C and Cocoa, and this example is about the most simple you can get, that you really should make the effort to get comfortable with it.

In Apple's View Controller Programming Guide they have this to say:

Dismissing a Presented View Controller

When it comes time to dismiss a presented view controller, the preferred approach is to let the presenting view controller dismiss it. In other words, whenever possible, the same view controller that presented the view controller should also take responsibility for dismissing it. Although there are several techniques for notifying the presenting view controller that its presented view controller should be dismissed, the preferred technique is delegation. For more information, see “Using Delegation to Communicate with Other Controllers.”

If you really think through what you want to achieve, and how you are going about it, you will realise that messaging your MainViewController to do all of the work is the only logical way out given that you don't want to use a NavigationController. If you do use a NavController, in effect you are 'delegating', even if not explicitly, to the navController to do all of the work. There needs to be some object that keeps a central track of what's going on with your VC navigation, and you need some method of communicating with it, whatever you do.

In practice Apple's advice is a little extreme... in normal cases, you don't need to make a dedicated delegate and method, you can rely on [self presentingViewController] dismissViewControllerAnimated: - it's when in cases like yours that you want your dismissing to have other effects on remote objects that you need to take care.

Here is something you could imagine to work without all the delegate hassle...

- (IBAction)dismiss:(id)sender {
    [[self presentingViewController] dismissViewControllerAnimated:YES 
                                                        completion:^{
        [self.presentingViewController performSelector:@selector(presentVC2:) 
                                            withObject:nil];
    }];

}

After asking the presenting controller to dismiss us, we have a completion block which calls a method in the presentingViewController to invoke VC2. No delegate needed. (A big selling point of blocks is that they reduce the need for delegates in these circumstances). However in this case there are a few things getting in the way...

  • in VC1 you don't know that mainVC implements the method present2 - you can end up with difficult-to-debug errors or crashes. Delegates help you to avoid this.
  • once VC1 is dismissed, it's not really around to execute the completion block... or is it? Does self.presentingViewController mean anything any more? You don't know (neither do I)... with a delegate, you don't have this uncertainty.
  • When I try to run this method, it just hangs with no warning or errors.

So please... take the time to learn delegation!

update2

In your comment you have managed to make it work by using this in VC2's dismiss button handler:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 

This is certainly much simpler, but it leaves you with a number of issues.

Tight coupling
You are hard-wiring your viewController structure together. For example, if you were to insert a new viewController before mainVC, your required behaviour would break (you would navigate to the prior one). In VC1 you have also had to #import VC2. Therefore you have quite a lot of inter-dependencies, which breaks OOP/MVC objectives.

Using delegates, neither VC1 nor VC2 need to know anything about mainVC or it's antecedents so we keep everything loosely-coupled and modular.

Memory
VC1 has not gone away, you still hold two pointers to it:

  • mainVC's presentedViewController property
  • VC2's presentingViewController property

You can test this by logging, and also just by doing this from VC2

[self dismissViewControllerAnimated:YES completion:nil]; 

It still works, still gets you back to VC1.

That seems to me like a memory leak.

The clue to this is in the warning you are getting here:

[self presentViewController:vc2 animated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
 // Attempt to dismiss from view controller <VC1: 0x715e460>
 // while a presentation or dismiss is in progress!

The logic breaks down, as you are attempting to dismiss the presenting VC of which VC2 is the presented VC. The second message doesn't really get executed - well perhaps some stuff happens, but you are still left with two pointers to an object you thought you had got rid of. (edit - I've checked this and it's not so bad, both objects do go away when you get back to mainVC)

That's a rather long-winded way of saying - please, use delegates. If it helps, I made another brief description of the pattern here:
Is passing a controller in a construtor always a bad practice?

update 3
If you really want to avoid delegates, this could be the best way out:

In VC1:

[self presentViewController:VC2
                   animated:YES
                 completion:nil];

But don't dismiss anything... as we ascertained, it doesn't really happen anyway.

In VC2:

[self.presentingViewController.presentingViewController 
    dismissViewControllerAnimated:YES
                       completion:nil];

As we (know) we haven't dismissed VC1, we can reach back through VC1 to MainVC. MainVC dismisses VC1. Because VC1 has gone, it's presented VC2 goes with it, so you are back at MainVC in a clean state.

It's still highly coupled, as VC1 needs to know about VC2, and VC2 needs to know that it was arrived at via MainVC->VC1, but it's the best you're going to get without a bit of explicit delegation.