Detect when a presented view controller is dismissed - ios

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.

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

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

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)

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
}

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

extension Foo: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
//call whatever you want
}
}
vc.presentationController?.delegate = foo

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

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

I have used deinit for the ViewController
deinit {
dataSource.stopUpdates()
}
A deinitializer is called immediately before a class instance is deallocated.

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.

#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

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

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:
popoverPresentationControllerShouldDismissPopover
popoverPresentationControllerDidDismissPopover
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.

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

Create one class file (.h/.m) and name it : DismissSegue
Select Subclass of : UIStoryboardSegue
Go to DismissSegue.m file & write down following code:
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
[sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Open storyboard & then Ctrl+drag from cancel button to VC1 & select Action Segue as Dismiss and you are done.

If you override on the view controller being dimissed:
override func removeFromParentViewController() {
super.removeFromParentViewController()
// your code here
}
At least this worked for me.

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/

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.

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

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.

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

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

Related

How to get notified when a presented view controller is dismissed with a gesture?

It is possible in some cases (iPhone X, iOS 13) to dismiss presented view controllers with a gesture, by pulling from the top.
In that case, I can't seem to find a way to notify the presenting view controller. Did I miss something?
The only I found would be to add a delegate method to the viewDidDisappear of the presented view controller.
Something like:
class Presenting: UIViewController, PresentedDelegate {
func someAction() {
let presented = Presented()
presented.delegate = self
present(presented, animated: true, completion: nil)
}
func presentedDidDismiss(_ presented: Presented) {
// Presented was dismissed
}
}
protocol PresentedDelegate: AnyObject {
func presentedDidDismiss(_ presented: Presented)
}
class Presented: UIViewController {
weak var delegate: PresentedDelegate?
override func viewDidDisappear(animated: Bool) {
...
delegate?.presentedDidDismiss(self)
}
}
It is also possible to manage this via notifications, using a vc subclass but it is still not satisfactory.
extension Notification.Name {
static let viewControllerDidDisappear = Notification.Name("UIViewController.viewControllerDidDisappear")
}
open class NotifyingViewController: UIViewController {
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .viewControllerDidDisappear, object: self)
}
}
There must be a better way to do this?
From iOS 13 Apple has introduced a new way for the users to dismiss the presented view controller by pulling it down from the top. This event can be captured by implementing the UIAdaptivePresentationControllerDelegate to the UIViewController you're presenting on, in this case, the Presenting controller. And then you can get notified about this event in the method presentationControllerDidDismiss. Here is the code example :-
class Presenting: UIViewController, UIAdaptivePresentationControllerDelegate {
func someAction() {
let presented = Presented()
presented.presentationController?.delegate = self
present(presented, animated: true, completion: nil)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Note:
This method only gets triggered for dismissing by swiping from the top and not for the programmatic dismiss(animated:,completion:) method.
You don't need any custom delegate or Notification observer for getting the event where the user dismisses the controller by swiping down, so you can remove them.
Adopt UIAdaptivePresentationControllerDelegate and implement presentationControllerDidAttemptToDismiss (iOS 13+)
extension Presenting : UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentingViewController.presentedDidDismiss(self)
}
}
UIPresentationController has a property presentingViewController. The name is self-explanatory. You don't need the explicit delegate protocol.
The method is actually called to be able to show a dialog for example to save changes before dismissing the controller. You can also implement presentationControllerDidDismiss()
And do not post notifications to controllers which are related to each other. That's bad practice.

Why does UIActivityViewController call viewWillDisappear() on the presenter?

I am presenting a UIActivityViewController to share an .mp4 video from a URL:
let viewController: UIViewController = ... // the presenting view controller
let url: URL = ... // local file
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
viewController.present(activityController, animated: false, completion: nil)
When the "save video" option is selected, the video is saved, but the presenting UIViewController disappears (and I can see that .viewWillDisappear() is called on it.)
How do I make the presenting UIViewController not disappear?
Note that all of the other share options that I've tried do not have this problem (messages, airdrop, instagram).
I have tried to set the sourceView and sourceRect, but it does not seem to help.
activityController.popoverPresentationController?.sourceView = viewController.view!
activityController.popoverPresentationController?.sourceRect = viewController.view!.frame
I've looked for errors, but didn't find any:
activityController.completionWithItemsHandler = { (a: UIActivity.ActivityType?, b: Bool, c: [Any]?, d: Error?) in
if let error = d {
print(error)
}
}
Also, all of my UIViewController lifecycle overrides call their super, i.e.:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
This is what it looks like:
It's bringing down my entire view with it!
For what it's worth, viewController is setup by calling a segue that is setup in a storyboard:
class LaunchController : UIViewController {
var performedSegue = false
override func viewDidLayoutSubviews() {
if !performedSegue {
self.performSegue(withIdentifier: "main", sender: self)
performedSegue = true
}
}
}
It looks like this "feature" was introduced by Apple in iOS 13. I tested
this on iOS 12 and the presenting ViewController does not disappear.
I traced the call stack and it appear that UIActivityViewController is calling dismiss on presenting view controller (or it's UINavigationController) when saving to camera roll succeeds.
I don't know the way to prevent this since it's Apple private API and there's nothing in documentation about this. The only way I found, is to to set some kind of flag savingToCameraRoll and set it to true when presenting UIActivityViewController, override dismiss method on presenting ViewController and then check this flag inside dismiss.
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if !savingToCameraRoll {
// Dismiss view controller if UIActivityViewController not in use
super.dismiss(animated: animated, completion: completion)
}
// Handle UIActivityViewController dismiss attempt.
// If you do nothing, the presenting ViewController should not be dismissed.
}
You should also remember to set savingToCameraRoll to false in completionWithItemsHandler.
In the storyboard, I marked the presenting ViewController as "Is Initial View Controller" instead of presenting it via a segue. That solved the issue for me (though it's a workaround that would not work in general).

navigationController:willShowViewController:animated: is not called after popToRootViewControllerAnimated in presented view controller

I have UINavigationController with several pushed view controllers.
UPD: Last pushed controller modally presents another controller.
Also, I have UINavigationControllerDelegate with some logic at navigationController:willShowViewController:animated:.
UPD: Navigation controller is its own delegate. Delegate is set in viewDidLoad method.
Question rises when I try to close all controllers programically from presented view controller:
// Close all controllers in navigation stack
presentingViewController?.navigationController?.popToRootViewController(animated: true)
// Close presented view controller
dismiss(animated: true, completion: nil)
Method navigationController:willShowViewController:animated: is not called. But it is called when I do the same without presented controller (thanks to #donmag for example project where it works).
Searched SO for answers or similar questions, but found nothing, any thoughts?
In your "presenting" VC, you want to implement a delegate/protocol pattern so your "presented" VC can call back and perform the dismiss and popToRoot...
// protocol for the presented VC to "call back" to the presenting VC
protocol dismissAndPopToRootProtocol {
func dismissAndPopToRoot(_ animated: Bool)
}
// in the presenting VC
#IBAction func presentTapped(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "presentMeVC") as? PresentMeViewController {
// Assign the delegate when instantiating and presenting the VC
vc.dapDelegate = self
present(vc, animated: true, completion: nil)
}
}
func dismissAndPopToRoot(_ animated: Bool) -> Void {
// this will dismiss the presented VC and then pop to root on the NavVC stack
dismiss(animated: animated, completion: {
self.navigationController?.popToRootViewController(animated: animated)
})
}
// in the presented VC
var dapDelegate: dismissAndPopToRootProtocol?
#IBAction func dismissTapped(_ sender: Any) {
// delegate/protocol pattern - pass true or false for dismiss/pop animation
dapDelegate?.dismissAndPopToRoot(false)
}
Here's a full demo project: https://github.com/DonMag/CustomNavController
From documentation:
popToRootViewControllerAnimated:
Pops all the view controllers on the stack except the root view controller and updates the display.
popViewControllerAnimated:
Pops the top view controller from the navigation stack and updates the display.
So seems like in order to get navigationController:willShowViewController:animated: called every time you have to do subsequent popViewControllerAnimated:, because the display got updated each time after you pop a new controller. When you pop to root view controller, update display is called only once.

Dismiss View Controller to Particular View Controller

My View Contollers
Login -> Main Menu -> A -> B -> C -> D
How do i dimiss all view controllers and go back to main menu
For Logout from my view controllers I am doing the following which takes back to Login
func logout{
self.view.window!.rootViewController?.dismissViewControllerAnimated(false, completion: nil)
}
Now what i am doing is this
class AppDelegate: UIResponder, UIApplicationDelegate {
var viewControllerStack: [BaseViewController]!
}
override func viewDidLoad() {
super.viewDidLoad()
super.appDelegateBase.viewControllerStack.append(self)
}
func go_To_MainMenu(){
var countOfNumberOfViewCOntrollers = self.appDelegateBase.viewControllerStack.count
switch countOfNumberOfViewCOntrollers{
self.presentingViewController?.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
break;
case 2:
self.presentingViewController?.presentingViewController?.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
break;
}
}
If your MainMenu VC always comes AFTER your Login VC, you could simply use the same method:
To MainMenu:
self.view.window!.rootViewController?.presentedViewController?.dismissViewControllerAnimated(true, completion: nil)
Instead of presenting/dismissing you can use UINavigationController to push/pop view controllers. That way you can use UINavigationController's popToViewController(_:animated:) which can pop to any view controller in navigation stack.
You can use Unwind Segue if MainMenu isn't your rootViewController. Look at this article, hope it will help.
Unwind Segue with Swift
Swift 5
This works for me
var presentingViewController = PresentingViewController()
self.dismiss(animated: false) {
presentingViewController.dismiss(animated: false, completion: nil)
}
Use unwind segue instead of using RootViewController.
Using unwind you can go back to any ViewController. DismissViewController always send the controller out from NavigationController.
This worked for me,
self.view.window!.rootViewController?.presentedViewController?.dismiss(animated: true, completion: nil)

How can you reload a ViewController after dismissing a modally presented view controller in Swift?

I have a first tableViewController which opens up a second tableViewcontroller upon clicking a cell. The second view controller is presented modally (Show Detail segue) and is dismissed with:
self.dismissViewControllerAnimated(true, completion: {})
At this point, the second view controller slides away and reveals the first view controller underneath it. I would then like to reload the first view controller. I understand that this may require use of delegate functions, but not sure exactly how to implement it
Swift 5:
You can access the presenting ViewController (presentingViewController) property and use it to reload the table view when the view will disappear.
class: FirstViewController {
var tableView: UITableView
present(SecondViewController(), animated: true, completion: nil)
}
In your second view controller, you can in the viewWillDisappear method, add the following code:
class SecondViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let firstVC = presentingViewController as? FirstViewController {
DispatchQueue.main.async {
firstVC.tableView.reloadData()
}
}
}
}
When you dismiss the SecondViewController, the tableview of the FirstViewController will reload.
I solved it a bit differently since I don't want that dependancy.
And this approach is intended when you present a controller modally, since the presenting controller wont reload when you dismiss the presented.
Anyway solution!
Instead you make a Singleton (mediator)
protocol ModalTransitionListener {
func popoverDismissed()
}
class ModalTransitionMediator {
/* Singleton */
class var instance: ModalTransitionMediator {
struct Static {
static let instance: ModalTransitionMediator = ModalTransitionMediator()
}
return Static.instance
}
private var listener: ModalTransitionListener?
private init() {
}
func setListener(listener: ModalTransitionListener) {
self.listener = listener
}
func sendPopoverDismissed(modelChanged: Bool) {
listener?.popoverDismissed()
}
}
Have you Presenting controller implement the protocol like this:
class PresentingController: ModalTransitionListener {
//other code
func viewDidLoad() {
ModalTransitionMediator.instance.setListener(self)
}
//required delegate func
func popoverDismissed() {
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
yourTableViev.reloadData() (if you use tableview)
}
}
and finally in your PresentedViewController in your viewDid/WillDisappear func or custom func add:
ModalTransitionMediator.instance.sendPopoverDismissed(true)
You can simply reaload your data in viewDidAppear:, but that might cause the table to be refreshed unnecessarily in some cases.
A more flexible solution is to use protocols as you have correctly guessed.
Let's say the class name of your first tableViewController is Table1VC and the second one is Table2VC. You should define a protocol called Table2Delegate that will contain a single method such as table2WillDismissed.
protocol Table2Delegate {
func table2WillDismissed()
}
Then you should make your Table1VC instance conform to this protocol and reload your table within your implementation of the delegate method.
Of course in order for this to work, you should add a property to Table2VC that will hold the delegate:
weak var del: Table2Delegate?
and set its value to your Table1VC instance.
After you have set your delegate, just add a call to the delegate method right before calling the dismissViewControllerAnimated in your Table2VC instance.
del?.table2WillDismissed()
self.dismissViewControllerAnimated(true, completion: {})
This will give you precise control over when the table will get reloaded.

Resources