Detect when a presented view controller is dismissed
Asked Answered
B

25

150

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.

Boarding answered 29/9, 2015 at 20:29 Comment(5)
in VC1 the viewWillAppear method will be calledEmancipated
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.Pondicherry
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.Pondicherry
@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.Boarding
A solution for this can be found in this related post: https://mcmap.net/q/160343/-presenting-view-controller-loses-subviews-when-dismissing-presented-vcTankard
P
79

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

Pondicherry answered 30/9, 2015 at 20:54 Comment(2)
Just wants to thanks and appreciate how beautifully this works..even after 4yrs! Thank you!Zealand
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.Frontal
E
118

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.
    }
}
Erikerika answered 12/3, 2019 at 8:46 Comment(10)
Easiest best answer, correctly addresses most of the problems and doesn't need extra implementations.Employment
It doesn't work correctly without pairing with viewDidAppear.Wallywalnut
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.Homocercal
viewDidDisappear is more appropriate methodCastellan
@Castellan That really depends on your use case.Erikerika
instead of "viewWillDisappear", you can use "isBeingDismissed" in "viewDidDisappear". override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isBeingDismissed { print("DISSMISSED!") } }Optical
doing this in viewDidDisappear did not work for me, it did in override func endAppearanceTransition()Gut
@Homocercal I keep getting falseMaragretmarala
Also if your UIViewController is wrapped with UINavigationController then you should use: navigationController?.isBeingDismissed ?? isBeingDismissedOrthopter
Anyway, this doesn't answer the question, because the question is regarding how to detect in VC1 that VC2 is being dismissed. Your answer tells how to detect in VC2 that VC2 is being dismissed.Orthopter
P
79

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

Pondicherry answered 30/9, 2015 at 20:54 Comment(2)
Just wants to thanks and appreciate how beautifully this works..even after 4yrs! Thank you!Zealand
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.Frontal
V
69

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)
Virus answered 10/8, 2017 at 17:14 Comment(5)
@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)Rouleau
@Rouleau 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.Virus
Assumes just one View Controller being presented. You could be on a navigation stack god knows where.Danby
@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 controllersCabby
Instead of force unwrapping the callback do it like this: onDoneBlock?(true) - this way it will safely not invoke the callback if it's nil.Caco
E
32

You can use UIViewControllerTransitioningDelegate on the parent view controller that you want to observe the dismissal of another presented view controller:

anotherViewControllerYouWantToObserve.transitioningDelegate = self

And observe the dismissal on:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    print("anotherViewControllerYouWantToObserve was dismissed")
    return nil
}
Eduction answered 2/6, 2021 at 13:16 Comment(5)
I believe the correct delegate to adopt is UIViewControllerTransitioningDelegate to observe forDismissedTragedy
It works well, yes but I believe you need to change UIAdaptivePresentationControllerDelegate to UIViewControllerTransitioningDelegate in your answerTragedy
I have edited the answer so that it states UIViewControllerTransitioningDelegate.Estimate
Does not work for all cases. Please beware that for modal presentations, animationController(forDismissed:) will be called when the the user first starts dragging the view controller to dismiss it, but they cancel the drag operation. This answer does not guarantee that the view controller was dismissed.Equipotential
Great Answer! Objective C version - -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ NSLog(@"valueDateChangeEditScrn was dismissed"); return nil; }Pernickety
C
19
extension Foo: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        //call whatever you want
    }
}

vc.presentationController?.delegate = foo
Cecilia answered 29/1, 2020 at 14:26 Comment(3)
iOS 13.0+ onlyCornemuse
also not called if the dismissal is done programaticallyPigling
perfect for my use-case. it is fired when the user 'cancels' by swiping down the view manuallyPierre
X
17

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...
}
Xantha answered 30/9, 2015 at 5:40 Comment(3)
No problem. But it turns out it doesn't work as expected. Thankfully, @RoryMcKinnel's answer seems to give more options.Xantha
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 controllerGodrich
It's not called when the user dismisses the modal view controller by a swipe from the top!Wallywalnut
G
7

This works well if you have a modal presentation that can be dismissed like a page sheet by swipe.

override func endAppearanceTransition() {
     if isBeingDismissed{
          print("dismissal logic here")
     }
     super.endAppearanceTransition()
 }
Gut answered 25/6, 2021 at 19:46 Comment(1)
This is 100% correct, good one! NOTE THOUGH THAT YOU DO NEED TO CALL SUPER for that call, surprisingly!Thapsus
C
6

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
    }
}
Coup answered 26/2, 2019 at 11:42 Comment(3)
Weird that this is the only solution that worked for me too.Broadbill
Only works when added or removed to/from a container VC.Zulazulch
sadly this does not work for a general modal caseMilled
E
3

I have used deinit for the ViewController

deinit {
    dataSource.stopUpdates()
}

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

Extrabold answered 20/7, 2020 at 10:6 Comment(2)
The issue with this solution is that if your viewController is retained somewhere or if you have a retain cycle in your viewController, this deinit will not be called when the viewController is dismissed.Westing
A bad idea, because if you initialize a ViewController in any method, and then the method stops executing, you will constantly have a deinitializer called and "phantom" bugs appear.Schach
M
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://mcmap.net/q/64180/-what-are-unwind-segues-for-and-how-do-you-use-them.

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.

Mozart answered 11/1, 2016 at 11:26 Comment(0)
I
2

@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

Icaria answered 22/2, 2016 at 5:20 Comment(0)
W
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?()
        }
    })
  }
}
Whitethroat answered 17/1, 2018 at 11:43 Comment(1)
You shouldn't inherit from AVPLayerViewController. Apple docs says: "Subclassing AVPlayerViewController and overridding its methods isn’t supported, and results in undefined behavior."Craftwork
O
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.

Oblivious answered 15/11, 2018 at 20:48 Comment(2)
And those are deprecated since iOS 13. DohLcm
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)Venetis
C
1

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

Cabby answered 27/1, 2020 at 19:34 Comment(0)
F
1

isBeingDismissed flag was false in both viewWillDisappear: and viewDidDisappear: when dismissing a controller that was presented as .formSheet on iPad in iOS 16

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    
    if self.view.window == nil {
        // view controller is being dismissed
    }
}

Note that in viewWillDisappear, window will still be valid, so make sure to use viewDidDisappear.

Fridafriday answered 21/6, 2023 at 7:40 Comment(1)
While that's a fantastic tip in many cases. if something else "covers" the presentation (example - a video player goes full screen) this reports incorrectly.Thapsus
R
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.

Rhpositive answered 30/9, 2015 at 5:21 Comment(0)
U
0

If you override on the view controller being dimissed:

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

At least this worked for me.

Unshackle answered 8/3, 2017 at 3:42 Comment(1)
@JohnScalo not true, quite a few of the “native” view controller hierarchies implement themselves with the child/parent primitives.Unshackle
P
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/

Presignify answered 6/6, 2017 at 7:49 Comment(0)
L
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.

Lingerie answered 18/2, 2018 at 10:46 Comment(2)
viewDidAppear?Wallywalnut
Did you mean viewDidDisappear?Aviate
C
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) } }

Courcy answered 26/7, 2018 at 15:46 Comment(0)
G
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.

Gulley answered 20/6, 2019 at 15:51 Comment(2)
But viewWillDisappear also doesn't work correctly without pairing with viewDidAppear.Wallywalnut
viewWillDisappear is called when the VC is completely covered (E.g. with a modal). You may not have been dismissedRearrange
M
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
        }
    }
Meimeibers answered 18/1, 2021 at 22:43 Comment(0)
M
0

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) {
    }
}
Mastoid answered 28/1, 2021 at 9:32 Comment(0)
G
0

Based on

https://mcmap.net/q/157202/-detect-when-a-presented-view-controller-is-dismissed (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. / This is also applicable when a user swipes from the screen edge and later decides to cancel such an operation)

and

https://mcmap.net/q/103795/-isbeingdismissed-not-set-in-viewwilldisappear

I think this is the accurate answer to date.

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    if isAboutToClose {
        if !deletedAttachments.isEmpty {
            imageViewDelegate?.attachmentsDeleted(deletedAttachments)
        }
    }
}

extension UIViewController {
    // https://mcmap.net/q/103795/-isbeingdismissed-not-set-in-viewwilldisappear
    var isAboutToClose: Bool {
        return
            self.isBeingDismissed ||
            self.isMovingFromParent ||
            self.navigationController?.isBeingDismissed ?? false
    }
}
Gerardgerardo answered 19/10, 2023 at 17:19 Comment(0)
S
0

If the presentationStyle is overFullScreen, you can use KVO to observe the presentationController, tested in iOS 17.

class ViewController: UIViewController {
    var observation: NSKeyValueObservation?
    override func viewDidLoad() {
        super.viewDidLoad()

        observation = observe(\.presentationController, options: [.old, .new]) { _, change in
            print("presentationController changed")
            if let vc = change.newValue as? UIPresentationController {
                print("presentedViewController: \(vc.presentedViewController.presentedViewController)")
            }
        }
    }

    deinit {
        observation?.invalidate()
    }
}

Please note that observing presentationController with KVO does not have official Apple documentation; therefore, it may be removed in the future.

Sunlight answered 21/3, 2024 at 15:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.