modal View controllers - how to display and dismiss
Asked Answered
G

6

82

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.

Gallup answered 16/2, 2013 at 6:6 Comment(0)
Y
190

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.

Yankee answered 16/2, 2013 at 12:49 Comment(20)
seems to be complicated. I tried to follow and copy to the dot but got lost in the middle. Is there any other way of achieving this?. I also wanted to add that in the app delegate, the main controller is set as the root view controller. I don't want to use Navigation controllers but wondering why this should be so complicated to achieve. To summarize, when the app starts, I show a main view controller with 2 buttons. Clicking on the first button loads VC1. There is a button on VC1 and on clicking it should load VC2 with no errors or warnings while at the same time dismiss VC1 from memory.Gallup
On VC2, I have a button and clicking it should dismiss VC2 from memory and the control should go back to the main controller and not go to VC1.Gallup
@Hema, I have perfectly understood your requirements, and assure you this is the correct way to do it. I've updated my answer with a little more info, hope that helps. If you tried my approach and got stuck, please raise a new question showing exactly what is not working so we can help. You can also link back to this question for clarity.Yankee
Hi He Was: Thanks for your insight. I'm also talking on another thread (original thread) and just posted a snippet from the suggestions mentioned there. I'm trying all expert answers to nail down this issue. The URL is here: #14840818Gallup
I followed your code step by step but I'm stuck at this line: "and in the @interface declare a property to hold the delegate pointer:". It is not clear which .h file and which .m file. I did the code on VC2 and the line "self.delegate" is not available. Could you please post a working code from your project line by line or zip the entire project and post it to a free server where I can pick it up and try myself. Sorry, this is dragging your time.Gallup
@Henna, i've updated that line to make it clearer. Also spotted a bad typo - that's supposed to be in VC1, not VC2, sorry...Yankee
Atlast! I found it. I never thought it would be so easy. I didn't have to use delegates at all. All I have to do is on VC2 button click, I do this: [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; and that's it. It navigated to my main view controller which is my root view controller as specified in the app delegate. The only thing I want to be sure is no memory leaks when going from main view controller to VC1 and then from VC1 to VC2 and then to main view controller from VC2.Gallup
@Hema, that's great that it is working, but please also check my update 2Yankee
thanks for your input. I understand that 2 pointers exist pointing to VC1. Now once I go to the main view controller and click on the VC1 button to go to VC1 again, what happens to the existing pointer to VC1?. Does it get updated with the existing reference from mainVC or does it add it up now to 3 pointers which I don't want.Gallup
@Hema, the memory issue isn't as bad as I thought, it's cleaned up when you get back to mainVC - but take a look at my update3 for probably the cleanest way to do this if you don't want to delegate.Yankee
Upvoted because this really helped me understand the flow of setting up a delegate, which was something i'd been struggling with for a while.Oxytocin
Good, thorough post. I got here from another post where somebody was confused and linked your post. One question, though. In your present1: method, you invoke a method present: which you don't define. Similarly, in dismissAndPresent2 and you invoke a method present2:. Are these shorthands for 'presentViewController:animated:completion:' Are they methods that are left as an exercise for the reader? You should explain what those methods are for completeness.Vicenta
Great detail in explanation, and give us more attention on delegate.Ret
I wish I could upvote this line of code a million times! [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];Fluke
AWESOME POST. I can understand everything but the purpose of update3, where should I message [self.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; ? And if I do such, then I'll be sent to MainVC, I could have simply done that doing a simple dismissViewController. Or was your purpose simply to demonstrate the weird syntax of self.presentingViewController.presentingViewController ?Dwarfism
I think your first line isn't sending a message to itself is slightly misleading. I mean it obviously sends a message to its presentingVC initially through itself right?Dwarfism
@Honey - see my other linked answer #14637391 ... it is initially going to itself but will always be forwarded. This is often not appreciated so was trying to be more explicit.Yankee
(read all 3 linked answers multiple times before, I think I get them, not sure :D). I'm not sure I follow your answer to my question of the purpose of update3, as it doesn't present VC2 which is what the OP is asking, it takes you to MainVC. Didn't you say so you are back at MainVC in a clean state.. ( I get the syntax, just don't know why you are suggesting it)Dwarfism
self.presentingViewController mean anything any more can't you just do a weak self?Dwarfism
@Honey - Perhaps so, but the statement was a rhetorical answer to a piece of 'imagined' pseudocode. The point I wanted to make isn't about retain cycles traps, but to educate the questioner as to why delegation is a valuable design pattern (which incidentally avoids that issue). I think that is the misleading contention here - the question is about modal VCs, but the the value of the answer lies mostly in its explication of the delegate pattern, using the question, and the OP's evident frustrations, as the catalyst. Thanks for your interest (and your edits)!!Yankee
D
13

Example in Swift, picturing the foundry's explanation above and the Apple's documentation:

  1. Basing on the Apple's documentation and the foundry's explanation above (correcting some errors), presentViewController version using delegate design pattern:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func dismissViewController1AndPresentViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        vc1.modalTransitionStyle = UIModalTransitionStyle.FlipHorizontal
        self.presentViewController(vc1, animated: true, completion: nil)
    }

    func dismissViewController1AndPresentViewController2() {
        self.dismissViewControllerAnimated(false, completion: { () -> Void in
            let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
            self.presentViewController(vc2, animated: true, completion: nil)
        })
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.dismissViewController1AndPresentViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
  1. Basing on the foundry's explanation above (correcting some errors), pushViewController version using delegate design pattern:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func popViewController1AndPushViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        self.navigationController?.pushViewController(vc1, animated: true)
    }

    func popViewController1AndPushViewController2() {
        self.navigationController?.popViewControllerAnimated(false)
        let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
        self.navigationController?.pushViewController(vc2, animated: true)
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.popViewController1AndPushViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
Dorsey answered 12/12, 2014 at 1:21 Comment(1)
in your example ViewController class is mainVC right?Dwarfism
M
10

I think you misunderstood some core concepts about iOS modal view controllers. When you dismiss VC1, any presented view controllers by VC1 are dismissed as well. Apple intended for modal view controllers to flow in a stacked manner - in your case VC2 is presented by VC1. You are dismissing VC1 as soon as you present VC2 from VC1 so it is a total mess. To achieve what you want, buttonPressedFromVC1 should have the mainVC present VC2 immediately after VC1 dismisses itself. And I think this can be achieved without delegates. Something along the lines:

UIViewController presentingVC = [self presentingViewController];
[self dismissViewControllerAnimated:YES completion:
 ^{
    [presentingVC presentViewController:vc2 animated:YES completion:nil];
 }];

Note that self.presentingViewController is stored in some other variable, because after vc1 dismisses itself, you shouldn't make any references to it.

Motoneuron answered 25/2, 2014 at 14:59 Comment(8)
so simple! i wish others would scroll down to your answer instead of stopping at the top post.Sibylle
in the OP's code, why doesn't [self dismiss...] happen after [self present...] is finished? It's not there is something asynchronous happeningDwarfism
@Honey actually, there is something asynchronous happening when calling presentViewController - that is why it has a completion handler. But even using that, if you dismiss the presenting view controller after it presents something, everything that it presents gets dismissed as well. So OP actually wants to present the viewcontroller from another presenter actually, so that it can dismiss the current oneMotoneuron
But even using that, if you dismiss the presenting view controller after it presents something, everything that it presents gets dismissed as well...Aha, so the compiler is basically saying "what you doing is stupid. You just undo-ed your previous line of code (as VC1 I'll dismiss myself and whatever I'm presenting). Don't do it" right?Dwarfism
The compiler won't "say" anything about it, and it might also be the case to not crash when executing this, just that it will behave in a way that the programmer doesn't expectMotoneuron
The OP has got a warning from compiler. That's what I meantDwarfism
@Honey, ah that's a warning from the runtime, not from the compiler. The platform has hooks to detect that something fishy is going on... a nice feature I thinkMotoneuron
I thought only the compiler can give warnings and at runtime you can only get errors. So you're saying you can get warnings at runtime too?Dwarfism
N
5

Radu Simionescu - awesome work! and below Your solution for Swift lovers:

@IBAction func showSecondControlerAndCloseCurrentOne(sender: UIButton) {
    let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("ConrollerStoryboardID") as UIViewControllerClass // change it as You need it
    var presentingVC = self.presentingViewController
    self.dismissViewControllerAnimated(false, completion: { () -> Void   in
        presentingVC!.presentViewController(secondViewController, animated: true, completion: nil)
    })
}
Normalcy answered 16/4, 2015 at 10:14 Comment(1)
this in a way makes me frustrated that it actually works.. I don't understand why the block doesn't capture "self.presentingViewController" and a strong reference is needed, i.e. "var presentingVC".. anyways, this works. thxCynarra
E
1

I wanted this:

MapVC is a Map in full screen.

When I press a button, it opens PopupVC (not in full screen) above the map.

When I press a button in PopupVC, it returns to MapVC, and then I want to execute viewDidAppear.

I did this:

MapVC.m: in the button action, a segue programmatically, and set delegate

- (void) buttonMapAction{
   PopupVC *popvc = [self.storyboard instantiateViewControllerWithIdentifier:@"popup"];
   popvc.delegate = self;
   [self presentViewController:popvc animated:YES completion:nil];
}

- (void)dismissAndPresentMap {
  [self dismissViewControllerAnimated:NO completion:^{
    NSLog(@"dismissAndPresentMap");
    //When returns of the other view I call viewDidAppear but you can call to other functions
    [self viewDidAppear:YES];
  }];
}

PopupVC.h: before @interface, add the protocol

@protocol PopupVCProtocol <NSObject>
- (void)dismissAndPresentMap;
@end

after @interface, a new property

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

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}
Epsilon answered 31/8, 2017 at 11:47 Comment(0)
G
1

I have solved the issue by using UINavigationController when presenting. In MainVC, when presenting VC1

let vc1 = VC1()
let navigationVC = UINavigationController(rootViewController: vc1)
self.present(navigationVC, animated: true, completion: nil)

In VC1, when I would like to show VC2 and dismiss VC1 in same time (just one animation), I can have a push animation by

let vc2 = VC2()
self.navigationController?.setViewControllers([vc2], animated: true)

And in VC2, when close the view controller, as usual we can use:

self.dismiss(animated: true, completion: nil)
Gyatt answered 2/11, 2017 at 17:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.