[ios] modal View controllers - how to display and dismiss

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.

Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to objective-c

Adding a UISegmentedControl to UITableView Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Warp \ bend effect on a UIView? Use NSInteger as array index Detect if the device is iPhone X Linker Command failed with exit code 1 (use -v to see invocation), Xcode 8, Swift 3 ITSAppUsesNonExemptEncryption export compliance while internal testing? How to enable back/left swipe gesture in UINavigationController after setting leftBarButtonItem? Change status bar text color to light in iOS 9 with Objective-C

Examples related to uiviewcontroller

How to set Status Bar Style in Swift 3 UIView touch event in controller How to lock orientation of one view controller to portrait mode only in Swift Programmatically navigate to another view controller/scene Adding a view controller as a subview in another view controller Changing the Status Bar Color for specific ViewControllers using Swift in iOS8 Get top most UIViewController Instantiate and Present a viewController in Swift How to check if a view controller is presented modally or pushed on a navigation stack? Add a UIView above all, even the navigation bar

Examples related to presentviewcontroller

Warning: Attempt to present * on * whose view is not in the window hierarchy - swift Swift presentViewController iOS: present view controller programmatically How to call a View Controller programmatically? modal View controllers - how to display and dismiss

Examples related to dismissviewcontroller

modal View controllers - how to display and dismiss