Modal View Controllers - How to Display and Dismiss

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 

- (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  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
// 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.

Handling dismissal of modal view controllers in Swift 3

This can be easily done using delegation.

Create a protocol in the modal view controller class(the view controller that is being presented).

protocol ModalViewControllerDelegate:class {
func dismissed()
}

Also create a delegate property and call the delegate method when the modal view controller is dismissed, say on tap of a button. Here is some code to help you with that.

class ModalViewController1: UIViewController {
var delegate:ModalViewControllerDelegate?

@IBAction func back(_ sender: Any) {
delegate?.dismissed()

}
}

Now in the presenting view controllers's prepare for segue method, make the presenting view controller as the delegate of the presented modal view controller. Heres some code.

import UIKit

class ViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "modal1" {
if let modalVC1 = segue.destination as? ModalViewController1 {
modalVC1.delegate = self
}
}
}
}



extension ViewController: ModalViewControllerDelegate {
func dismissed() {
dismiss(animated: true, completion: nil)//dismiss the presented view controller
//do whatever you want after it is dismissed
//like opening another modal view controller
}
}

The dismissed function will be called after the modal view controller's back button is tapped and you can dismiss the presented view controller and also proceed with whatever you want to do after that.

How to dismiss 2 modal view controllers without weird animation

Create a snapshot from the currently visible view and add it as a subview to the first presented view controller. To find that you can simply "loop through" the presenting view controllers and dismiss from the initial one:

@IBAction func dismissViewControllers(_ sender: UIButton) {
var initialPresentingViewController = self.presentingViewController
while let previousPresentingViewController = initialPresentingViewController?.presentingViewController {
initialPresentingViewController = previousPresentingViewController
}


if let snapshot = view.snapshotView(afterScreenUpdates: true) {
initialPresentingViewController?.presentedViewController?.view.addSubview(snapshot)
}

initialPresentingViewController?.dismiss(animated: true)
}

This is the result with slow animations enabled for the dismissal:
https://www.dropbox.com/s/tjkthftuo9kqhsg/result.mov?dl=0

Dismiss current modal view controller and then present new modal view controller

I will try to give a more through answer here. When programming in iOS all instances must be created from... other instances. When I say instance I mean an object that has been instantiated (an object that has been created in the computers memory). So, whenever you make a project with XCode, you always need to mark 'the initial view controller'. (See that the bottom right says 'is initial View Controller, ask yourself... why does XCode really need to know that?')This is going to be the first instance that you create. Then it is used to spawn other instances. The reason why Apple chose this architecture is for security reasons (I think...? someone correct me if they have a better answer). You can see a very clear view of all the 'instances' of the views. You click this button while you are running a program in XCode Sample Image. Then you can see the hierarchy of the views. I have made a simple program where clicking a button will load a different view. Where here I have not clicked the button and I have only loaded one view. Before I click the button and add a new view However here I have clicked the button and loaded the next view, Sample Image. At the top they both say UIWindow. That is because... (I hope you can guess this part!) They are both being instantiated from the UIWindow View. Don't believe me? Check this out! Here are the actual view hierarchies.The one where the button has not been pressed and The one where the button has been pressed. So, in the case where you dont get what my point is. You should understand that simply by reading your question it is pretty obvious that you are trying to instantiate your model controller from another one that you are trying to dismiss. So if the model controller has been dismissed how is it supposed to instantiate another model controller? Also keep in mind that it is better to just name your controllers after their purpose like, MenuViewController, or MainController, or VideoController. The word model is usually used in MVC and it should not be used as part of a ViewControllers name. (Hope Im not sounding rude lol, I used to tutor junior high so this is how I teach XD )

Dismiss and present modal view controller with one animation

My solution to this issue was replacing current rootViewController, supporting different transitions:

static func replaceRootViewController(with viewController: UIViewController, transition: UIViewAnimationOptions, completion: (() -> ())? = nil) {        
if transition == .transitionCrossDissolve {
let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false)
viewController.view.addSubview(overlayView)
UIApplication.shared.keyWindow?.rootViewController = viewController

UIView.animate(withDuration: 0.65, delay: 0, options: transition, animations: {
overlayView.alpha = 0
}, completion: { finished in
overlayView.removeFromSuperview()
if let completion = completion{
completion()
}
})
} else {
_ = viewController.view
UIView.transition(with: UIApplication.shared.keyWindow!, duration: 0.65,options: transition, animations: {
UIApplication.shared.keyWindow?.rootViewController = viewController
}){_ in
if let completion = completion {
completion()
}

}
}
}

Unable to dismiss the modal view controller

let layout = UICollectionViewFlowLayout()
let navController = UINavigationController(rootViewController: HomeController(collectionViewLayout: layout))
window?.rootViewController = navController

Try this.



Related Topics



Leave a reply



Submit