Detect when a presented view controller is dismissed

IosUiviewcontroller

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

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

Solution 2 - Ios

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

Solution 3 - Ios

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)

Solution 4 - Ios

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

Solution 5 - Ios

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
}

Solution 6 - Ios

extension Foo: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        //call whatever you want
    }
}

vc.presentationController?.delegate = foo

Solution 7 - Ios

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

Solution 8 - Ios

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")
            }
 }

Solution 9 - Ios

I have used deinit for the ViewController

deinit {
    dataSource.stopUpdates()
}

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

Solution 10 - Ios

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.

Solution 11 - Ios

@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

Solution 12 - Ios

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?()
        }
    })
  }
}

Solution 13 - Ios

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.

Solution 14 - Ios

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

Solution 15 - Ios

  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.

Solution 16 - Ios

If you override on the view controller being dimissed:

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

At least this worked for me.

Solution 17 - Ios

Solution 18 - Ios

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.

Solution 19 - Ios

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) } }

Solution 20 - Ios

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.

Solution 21 - Ios

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

Solution 22 - Ios

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) {
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser523234View Question on Stackoverflow
Solution 1 - IosJoris WeimarView Answer on Stackoverflow
Solution 2 - IosRory McKinnelView Answer on Stackoverflow
Solution 3 - IosbrycejlView Answer on Stackoverflow
Solution 4 - IosNicolas MiariView Answer on Stackoverflow
Solution 5 - IosSenocico StelianView Answer on Stackoverflow
Solution 6 - IosIvanView Answer on Stackoverflow
Solution 7 - IosShaviView Answer on Stackoverflow
Solution 8 - IoslandnblocView Answer on Stackoverflow
Solution 9 - IosDan AlboteanuView Answer on Stackoverflow
Solution 10 - IosvalarMorghulisView Answer on Stackoverflow
Solution 11 - IosAndrew CoadView Answer on Stackoverflow
Solution 12 - IosfruitcoderView Answer on Stackoverflow
Solution 13 - IosIzhidoView Answer on Stackoverflow
Solution 14 - IosIgor VasilevView Answer on Stackoverflow
Solution 15 - IosTapansinh SolankiView Answer on Stackoverflow
Solution 16 - IosmxclView Answer on Stackoverflow
Solution 17 - IosSavas AdarView Answer on Stackoverflow
Solution 18 - IosA TView Answer on Stackoverflow
Solution 19 - IosSteveView Answer on Stackoverflow
Solution 20 - IosArthur KvaratskhelijaView Answer on Stackoverflow
Solution 21 - IosOpenningappsView Answer on Stackoverflow
Solution 22 - IosAshkan GhodratView Answer on Stackoverflow