92

Let's say, I have an instance of a view controller class called VC2. In VC2, there is a "cancel" button that will dismiss itself. But I can't detect or receive any callback when the "cancel" button got trigger. VC2 is a black box.

A view controller (called VC1) will present VC2 using presentViewController:animated:completion: method.

What options does VC1 have to detect when VC2 was dismissed?

Edit: From the comment of @rory mckinnel and answer of @NicolasMiari, I tried the following:

In VC2:

-(void)cancelButton:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:^{

    }];
//    [super dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

In VC1:

//-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
- (void)dismissViewControllerAnimated:(BOOL)flag
                           completion:(void (^ _Nullable)(void))completion
{
    NSLog(@"%s ", __PRETTY_FUNCTION__);
    [super dismissViewControllerAnimated:flag completion:completion];
//    [self dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

But the dismissViewControllerAnimated in the VC1 was not getting called.

user523234
  • 12,877
  • 8
  • 55
  • 98
  • 2
    in VC1 the viewWillAppear method will be called – Istvan Sep 29 '15 at 20:43
  • 1
    According to the docs, the presenting controller is responsible for the actual dismiss. When the presented controller dismisses itself, it will ask the presenter to do it for it. So if you override `dismissViewControllerAnimated` in your VC1 controller I believe it will get called when you hit cancel on VC2. Detect the dismiss and then call the super classes version which will do the actual dismiss. – Rory McKinnel Sep 29 '15 at 21:14
  • 1
    You can test your override by calling `[self.presentingViewController dismissViewControllerAnimated]`. It may be that the inner code has a different mechanism for asking the presenter to do the dismiss. – Rory McKinnel Sep 30 '15 at 14:26
  • @RoryMcKinnel: Using self.presentingViewController did work in my lab VC2 as well as from the real black box. If you put your comments in the answer then I will select it as the answer. Thanks. – user523234 Sep 30 '15 at 19:25
  • A solution for this can be found in this related post: http://stackoverflow.com/a/34571641/3643020 – Campbell_Souped Jan 03 '16 at 00:04

20 Answers20

65

According to the docs, the presenting controller is responsible for the actual dismiss. When the presented controller dismisses itself, it will ask the presenter to do it for it. So if you override dismissViewControllerAnimated in your VC1 controller I believe it will get called when you hit cancel on VC2. Detect the dismiss and then call the super classes version which will do the actual dismiss.

As found from discussion this does not seem to work. Rather than rely on the underlying mechanism, instead of calling dismissViewControllerAnimated:completion on VC2 itself, call dismissViewControllerAnimated:completion on self.presentingViewController in VC2. This will then call your override directly.

A better approach altogether would be to have VC2 provide a block which is called when the modal controller has completed.

So in VC2, provide a block property say with the name onDoneBlock.

In VC1 you present as follows:

  • In VC1, create VC2

  • Set the done handler for VC2 as: VC2.onDoneBlock={[VC2 dismissViewControllerAnimated:YES completion:nil]};

  • Present the VC2 controller as normal using [self presentViewController:VC2 animated:YES completion:nil];

  • In VC2, in the cancel target action call self.onDoneBlock();

The result is VC2 tells whoever raises it that it is done. You can extend the onDoneBlock to have arguments which indicate if the modal comleted, cancelled, succeeded etc....

Rory McKinnel
  • 7,730
  • 2
  • 15
  • 28
  • 2
    Just wants to thanks and appreciate how beautifully this works..even after 4yrs! Thank you! – Anna Apr 16 '19 at 15:30
  • 1
    I was going through this. If I dismiss the controller in the presenting controller VC1, the VC2 deinit is never called. Might be leading to retain cycles as the closure `onDoneBlock` is not weak. So, removing the line of dismissing V2 controller in block and calling the block in VC2, and in the next line in the same VC2, dismissing the controller in VC2 itself solve the problem. This ensured me to detect when a presented controller is dismissed with the block called and deinit of VC2 is also called. – Rajan Maheshwari Apr 18 '21 at 08:14
57

There is a special Boolean property inside UIViewController called isBeingDismissed that you can use for this purpose:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if isBeingDismissed {
        // TODO: Do your stuff here.
    }
}
Joris Weimar
  • 3,966
  • 3
  • 29
  • 48
  • 4
    Easiest best answer, correctly addresses most of the problems and doesn't need extra implementations. – reojased Jul 30 '19 at 04:05
  • It doesn't work correctly without pairing with `viewDidAppear`. – Dmitry Jan 28 '20 at 03:29
  • 15
    In an iOS13 modal presentation this will be true when a user starts dragging the controller to dismiss, but they can choose not to complete the dismissal. – Estel Feb 28 '20 at 13:08
  • 2
    `viewDidDisappear` is more appropriate method – kasyanov-ms Mar 09 '21 at 09:43
  • @kasyanov-ms That really depends on your use case. – Joris Weimar Mar 09 '21 at 19:36
  • 2
    instead of "viewWillDisappear", you can use "isBeingDismissed" in "viewDidDisappear". override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isBeingDismissed { print("DISSMISSED!") } } – Michael42 Mar 19 '21 at 14:30
45

Use a Block Property

Declare in VC2

var onDoneBlock : ((Bool) -> Void)?

Setup in VC1

VC2.onDoneBlock = { result in
                // Do something
            }

Call in VC2 when you're about to dismiss

onDoneBlock!(true)
brycejl
  • 1,134
  • 11
  • 21
  • @Bryce64 It's not working for me, I got "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value", at the point where the code goes to onDoneBlock!(true) – Lucas Feb 21 '18 at 15:09
  • @Lucas Sounds like you didn't declare it correctly in VC1. The "!" forces the unwrap to force an error if you don't have it set up correctly. – brycejl Feb 21 '18 at 16:54
  • 1
    Assumes just one View Controller being presented. You could be on a navigation stack god knows where. – Lee Probert Jul 24 '18 at 10:56
  • @LeeProbert Exactly. We have a presented Navigation controller with about 10 possible child controllers itside of its stack, and almost every one of them can trigger the dismissal... in this situation any completion block would have to be passed to all 10 such controllers – Igor Vasilev Jan 27 '20 at 17:16
14

Both the presenting and presented view controller can call dismissViewController:animated: in order to dismiss the presented view controller.

The former option is (arguably) the "correct" one, design-wise: The same "parent" view controller is responsible for both presenting and dismissing the modal ("child") view controller.

However, the latter is more convenient: typically, the "dismiss" button is attached to the presented view controller's view, and it has said view controller set as its action target.

If you are adopting the former approach, you already know the line of code in your presenting view controller where the dismissal occurs: either run your code just after dismissViewControllerAnimated:completion:, or within the completion block.

If you are adopting the latter approach (presented view controller dismisses itself), keep in mind that calling dismissViewControllerAnimated:completion: from the presented view controller causes UIKit to in turn call that method on the presenting view controller:

Discussion

The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.

(source: UIViewController Class Reference)

So, in order to intercept such event, you could override that method in the presenting view controller:

override func dismiss(animated flag: Bool,
                         completion: (() -> Void)?) {
    super.dismiss(animated: flag, completion: completion)

    // Your custom code here...
}
Michael Dautermann
  • 86,557
  • 17
  • 155
  • 196
Nicolas Miari
  • 15,044
  • 6
  • 72
  • 173
  • 1
    No problem. But it turns out it doesn't work as expected. Thankfully, @RoryMcKinnel's answer seems to give more options. – Nicolas Miari Oct 02 '15 at 02:47
  • Though this approach is generic enough for sub classing the view controller from a base view controller ad overriding dismissViewControllerAnimated in that. But it fails If you try to wrap up in a view controler in navigation view controller – hariszaman Jan 24 '18 at 07:24
  • 4
    It's not called when the user dismisses the modal view controller by a swipe from the top! – Dmitry Jan 28 '20 at 03:29
6
extension Foo: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        //call whatever you want
    }
}

vc.presentationController?.delegate = foo
Ivan
  • 909
  • 12
  • 23
4

Using the willMove(toParent: UIViewController?) in the following way seemed to work for me. (Tested on iOS12).

override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent);

    if parent == nil
    {
        // View controller is being removed.
        // Perform onDismiss action
    }
}
Shavi
  • 101
  • 4
2

You can use unwind segue to do this task, no need to use the dismissModalViewController. Define an unwind segue method in your VC1.

See this link on how to create the unwind segue, https://stackoverflow.com/a/15839298/5647055.

Assuming your unwind segue is set up, in the action method defined for your "Cancel" button, you can perform the segue as -

[self performSegueWithIdentifier:@"YourUnwindSegueName" sender:nil];

Now, whenever you press the "Cancel" button in the VC2, it will be dismissed and VC1 will appear. It will also call the unwind method, you defined in VC1. Now, you know when the presented view controller is dismissed.

Community
  • 1
  • 1
valarMorghulis
  • 302
  • 1
  • 12
2

I use the following to signal to a coordinator that the view controller is "done". This is used in a AVPlayerViewController subclass in a tvOS application and will be called after the playerVC dismissal transition has completed:

class PlayerViewController: AVPlayerViewController {
  var onDismissal: (() -> Void)?

  override func beginAppearanceTransition(_ isAppearing: Bool, animated: Bool) {
    super.beginAppearanceTransition(isAppearing, animated: animated)
    transitionCoordinator?.animate(alongsideTransition: nil,
      completion: { [weak self] _ in
         if !isAppearing {
            self?.onDismissal?()
        }
    })
  }
}
fruitcoder
  • 514
  • 4
  • 17
  • You shouldn't inherit from AVPLayerViewController. Apple docs says: "Subclassing AVPlayerViewController and overridding its methods isn’t supported, and results in undefined behavior." – Neru Jun 01 '20 at 11:04
1

@user523234 - "But the dismissViewControllerAnimated in the VC1 was not getting called."

You can't assume that VC1 actually does the presenting - it could be the root view controller, VC0, say. There are 3 view controllers involved:

  • sourceViewController
  • presentingViewController
  • presentedViewController

In your example, VC1 = sourceViewController, VC2 = presentedViewController, ?? = presentingViewController - maybe VC1, maybe not.

However, you can always rely on VC1.animationControllerForDismissedController being called (if you have implemented the delegate methods) when dismissing VC2 and in that method you can do what you want with VC1

sKhan
  • 7,934
  • 16
  • 51
  • 51
Andrew Coad
  • 257
  • 2
  • 11
1

I've seen this post so many times when dealing with this issue, I thought I might finally shed some light on a possible answer.

If what you need is to know whether user-initiated actions (like gestures on screen) engaged dismissal for an UIActionController, and don't want to invest time in creating subclasses or extensions or whatever in your code, there is an alternative.

As it turns out, the popoverPresentationController property of an UIActionController (or, rather, any UIViewController to that effect), has a delegate you can set anytime in your code, which is of type UIPopoverPresentationControllerDelegate, and has the following methods:

Assign the delegate from your action controller, implement your method(s) of choice in the delegate class (view, view controller or whatever), and voila!

Hope this helps.

Izhido
  • 353
  • 3
  • 10
  • And those are deprecated since iOS 13. Doh – Ben Affleck Jun 02 '20 at 07:06
  • No fizzle, no shizzle. But for iPad only. Seems like there is no way else than subclassing UIActivityViewController. (The idea is not bad at all; you almost got an upvote on your answer) – Anticro Feb 18 '21 at 19:58
1

Another option is to listen to dismissalTransitionDidEnd() of your custom UIPresentationController

Igor Vasilev
  • 316
  • 3
  • 6
1

A more productive approach would be to create a protocol for presentingControllers and then call in childControllers

protocol DismissListener {
    
    func childControllerWillDismiss(_ controller : UIViewController,  animated : Bool)
    func childControllerDidDismiss(_ controller : UIViewController,  animated : Bool)
}

extension UIViewController {
    
    func dismissWithListener(animated flag: Bool, completion: (() -> Void)? = nil){
        
        self.viewWillDismiss(flag)
        self.dismiss(animated: flag, completion: {
            completion?()
            self.viewDidDismiss(true)
        })
    }
    
    func viewWillDismiss(_ animate : Bool) {
        (presentingViewController as? DismissListener)?.childControllerWillDismiss(self, animated: animate)
    }
    
    func viewDidDismiss(_ animate : Bool) {
        (presentingViewController as? DismissListener)?.childControllerDidDismiss(self, animated: animate)
    }
}

and then when the view is about to dismiss :

self.dismissWithListener(animated: true, completion: nil)

and finally just add protocol to any viewController that you wish to listen!

class ViewController: UIViewController, DismissListener {

    func childControllerWillDismiss(_ controller: UIViewController, animated: Bool) {
    }
    
    func childControllerDidDismiss(_ controller: UIViewController, animated: Bool) {
    }
}
Ashkan Ghodrat
  • 2,960
  • 2
  • 30
  • 34
0
  1. Create one class file (.h/.m) and name it : DismissSegue
  2. Select Subclass of : UIStoryboardSegue

  3. Go to DismissSegue.m file & write down following code:

    - (void)perform {
        UIViewController *sourceViewController = self.sourceViewController;
        [sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
    
  4. Open storyboard & then Ctrl+drag from cancel button to VC1 & select Action Segue as Dismiss and you are done.

Nicolas Miari
  • 15,044
  • 6
  • 72
  • 173
0

If you override on the view controller being dimissed:

override func removeFromParentViewController() {
    super.removeFromParentViewController()
    // your code here
}

At least this worked for me.

mxcl
  • 24,446
  • 11
  • 91
  • 95
  • 1
    @JohnScalo not true, quite a few of the “native” view controller hierarchies implement themselves with the child/parent primitives. – mxcl Nov 16 '18 at 22:46
0

You can handle uiviewcontroller closed using with Unwind Segues.

https://developer.apple.com/library/content/technotes/tn2298/_index.html

https://spin.atomicobject.com/2014/12/01/program-ios-unwind-segue/

Savas Adar
  • 3,357
  • 2
  • 38
  • 48
0

overrideing viewDidAppear did the trick for me. I used a Singleton in my modal and am now able to set and get from that within the calling VC, the modal, and everywhere else.

A T
  • 10,508
  • 14
  • 85
  • 137
0

As has been mentioned, the solution is to use override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).

For those wondering why override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) does not always seem to work, you may find that the call is being intercepted by a UINavigationControllerif it's being managed. I wrote a subclass that should help:

class DismissingNavigationController: UINavigationController { override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { super.dismiss(animated: flag, completion: completion) topViewController?.dismiss(animated: flag, completion: completion) } }

Steve
  • 11
  • 2
0

If you want to handle view controller dismissing, you should use code below.

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.isBeingDismissed && self.completion != NULL) {
        self.completion();
    }
}

Unfortunately we can't call completion in overridden method - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion; because this method is been called only if you call dismiss method of this view controller.

  • But `viewWillDisappear` also doesn't work correctly without pairing with `viewDidAppear`. – Dmitry Jan 28 '20 at 03:30
  • viewWillDisappear is called when the VC is completely covered (E.g. with a modal). You may not have been dismissed – Lou Franco Mar 14 '20 at 01:18
0

I have used deinit for the ViewController

deinit {
    dataSource.stopUpdates()
}

A deinitializer is called immediately before a class instance is deallocated.

Dan Alboteanu
  • 6,336
  • 1
  • 37
  • 33
0

I didn't see what seems to be an easy answer. Pardon me if this is a repeat...

Since VC1 is in charge of dismissing VC2, then you need to have called vc1.dismiss() at some point. So you can just override dismiss() in VC1 and put your action code in there:

class VC1 : UIViewController {
    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        super.dismiss(animated: flag, completion: completion)
        // PLACE YOUR ACTION CODE HERE
    }
}

EDIT: You probably want to trigger your code when the dismiss completes, not when it starts. So in that case, you should use:

    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        super.dismiss(animated: flag) {
            if let unwrapCompletion = completion { unwrapCompletion() }
            // PLACE YOUR ACTION HERE
        }
    }