Before iOS 13, presented view controllers used to cover the entire screen. And, when dismissed, the parent view controller viewDidAppear function were executed.
Now iOS 13 will present view controllers as a sheet as default, which means the card will partially cover the underlying view controller, which means that viewDidAppear will not be called, because the parent view controller has never actually disappeared.
Is there a way to detect that the presented view controller sheet was dismissed? Some other function I can override in the parent view controller rather than using some sort of delegate?
Is there a way to detect that the presented view controller sheet was dismissed?
Yes.
Some other function I can override in the parent view controller rather than using some sort of delegate?
No. "Some sort of delegate" is how you do it. Make yourself the presentation controller's delegate and override presentationControllerDidDismiss(_:).
https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229889-presentationcontrollerdiddismiss
The lack of a general runtime-generated event informing you that a presented view controller, whether fullscreen or not, has been dismissed, is indeed troublesome; but it's not a new issue, because there have always been non-fullscreen presented view controllers. It's just that now (in iOS 13) there are more of them! I devote a separate question-and-answer to this topic elsewhere: Unified UIViewController "became frontmost" detection?.
Here's a code example of a parent view-controller which is notified when the child view-controller it presents as a sheet (i.e., in the default iOS 13 manner) is dismissed:
public final class Parent: UIViewController, UIAdaptivePresentationControllerDelegate
{
// This is assuming that the segue is a storyboard segue;
// if you're manually presenting, just set the delegate there.
public override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "mySegue" {
segue.destination.presentationController?.delegate = self;
}
}
public 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).
}
}
Jerland2's answer is confused, since (a) the original questioner wanted to get a function call when the sheet is dismissed (whereas he implemented presentationControllerDidAttemptToDismiss, which is called when the user tries and fails to dismiss the sheet), and (b) setting isModalInPresentation is entirely orthogonal and in fact will make the presented sheet undismissable (which is the opposite of what OP wants).
For future readers here is a more complete answer with implementation:
In the root view controllers prepare for segue add the following (Assuming your modal has a nav controller)
// Modal Dismiss iOS 13
modalNavController.presentationController?.delegate = modalVc
In the modal view controller add the following delegate + method
// MARK: - iOS 13 Modal (Swipe to Dismiss)
extension ModalViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
print("slide to dismiss stopped")
self.dismiss(animated: true, completion: nil)
}
}
Ensure in the modal View Controller that the following property is true in order for the delegate method to be called
self.isModalInPresentation = true
Profit
Another option to get back viewWillAppear and viewDidAppear is set
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen
this option cover full screen and after dismiss, calls above methods
Swift
General Solution to call viewWillAppear in iOS13
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear")
}
//Show new viewController
#IBAction func show(_ sender: Any) {
let newViewController = NewViewController()
//set delegate of UIAdaptivePresentationControllerDelegate to self
newViewController.presentationController?.delegate = self
present(newViewController, animated: true, completion: nil)
}
}
extension UIViewController: UIAdaptivePresentationControllerDelegate {
public func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
if #available(iOS 13, *) {
//Call viewWillAppear only in iOS 13
viewWillAppear(true)
}
}
}
If you want to do something when user closes the modal sheet from within that sheet.
Let's assume you already have some Close button with an #IBAction and a logic to show an alert before closing or do something else. You just want to detect the moment when user makes push down on such a controller.
Here's how:
class MyModalSheetViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.presentationController?.delegate = self
}
#IBAction func closeAction(_ sender: Any) {
// your logic to decide to close or not, when to close, etc.
}
}
extension MyModalSheetViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false // <-prevents the modal sheet from being closed
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
closeAction(self) // <- called after the modal sheet was prevented from being closed and leads to your own logic
}
}
Override viewWillDisappear on the UIViewController that's being dismissed. It will alert you to a dismissal via isBeingDismissed boolean flag.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
print("user is dismissing the vc")
}
}
** If the user is halfway through the swipe down and swipes the card back up, it'll still register as being dismissed, even if the card is not dismissed. But that's an edge case you may not care about.
DRAG OR CALL DISMISS FUNC will work with below code.
1) In root view controller, you tell that which is its presentation view controller as below code
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "presenterID" {
let navigationController = segue.destination as! UINavigationController
if #available(iOS 13.0, *) {
let controller = navigationController.topViewController as! presentationviewcontroller
// Modal Dismiss iOS 13
controller.presentationController?.delegate = self
} else {
// Fallback on earlier versions
}
navigationController.presentationController?.delegate = self
}
}
2) Again in the root view controller, you tell what you will do when its presentation view controller is dissmised
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
print("presentationControllerDidDismiss")
}
1) In the presentation view controller, When you hit cancel or save button in this picture. Below code will be called.The
self.dismiss(animated: true) {
self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
}
in SwiftUI you can use onDismiss closure
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
If someone doesn't have access to the presented view controller, they can just override the following method in presenting view controller and change the modalPresentationStyle to fullScreen or can add one of the strategies mentioned above with this approach
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let _ = viewControllerToPresent as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
if presented view controller is navigation controller and you want to check the root controller, can change the above condition to be like
if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
If you used the ModalPresentationStyle in FullScreen, the behavior of the controller is back as usual.
ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController;
controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
this.NavigationController.PushViewController(controllerConsultar, true);
From my point of view, Apple should not set pageSheet is the default modalPresentationStyle
I'd like to bring fullScreen style back to default by using swizzling
Like this:
private func _swizzling(forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIViewController {
static func preventPageSheetPresentationStyle () {
UIViewController.preventPageSheetPresentation
}
static let preventPageSheetPresentation: Void = {
if #available(iOS 13, *) {
_swizzling(forClass: UIViewController.self,
originalSelector: #selector(present(_: animated: completion:)),
swizzledSelector: #selector(_swizzledPresent(_: animated: completion:)))
}
}()
#available(iOS 13.0, *)
private func _swizzledPresent(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
if viewControllerToPresent.modalPresentationStyle == .pageSheet
|| viewControllerToPresent.modalPresentationStyle == .automatic {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
_swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
}
}
And then put this line to your AppDelegate
UIViewController.preventPageSheetPresentationStyle()
wouldn't it be simple to call the presentingViewController.viewWillAppear?
befor dismissing?
self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
I have a parent UIViewController(MainVC). From there I have 2 segue to 2 UIViewControllers: FirstVC (identifier: goFirstVC) and SecondVC (identifier: goSecondVC)
In FirstVC I have a button Save and when I click it I want to dismiss the FirstVC and to go on SecondVC.
Here is my code:
#IBAction func saveBtnTapped(_ sender: UIButton) {
//navigationController?.popViewController(animated: true)
let destinationController = self.storyboard?.instantiateViewController(withIdentifier: "goSecondVC") as! SecondVC
let presentingVC = self.presentingViewController
self.dismiss(animated: false, completion: { () -> Void in
presentingVC!.present(destinationController, animated: true, completion: nil)
})
}
Here is the design for my issue:
You can use setViewControllers to keep the parent only and the SecondVC
let destinationController = self.storyboard?.instantiateViewController(withIdentifier: "goSecondVC") as! SecondVC
self.navigationController?.setViewControllers([self.navigationController!.viewControllers.first!,destinationController], animated: true)
There are many methods but one of the generic one is to use delegates and protocols. Use the following code in your classes.
Add Following code in the first VC
protocol SecondVCDelegate : AnyObject {
func goToSecondVC()
}
class FirstVC: UIViewController {
var Delegate : SecondVCDelegate!
#objc func save() {
Delegate.goToSecondVC()
}
}
//In second view
Add following code in MinVC
class MainVC: UIViewController {
override func viewDidAppear() {
self.performSegue(withIdentifier: <yourSegueIdentifierToFirstVC>, sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == <yourSegueIdentifierToFirstVC> {
let cont = segue.destination as! FirstVC
cont.Delegate = self
}
}
}
extension MainVC : SecondVCDelegate {
func goToSecondVC() {
self.performSegue(withIdentifier: <yourSegueIdentifierToSecondVC>, sender: nil)
}
}
//This is MainVC
I think you should present second from first rather than dismissing first then presenting second.
when you will present second from first then if you wanto go back to main you can simply dismiss 2 veiwcontrollers at once without any hack.
try it:
We can control our presented controllers with navigation controller calling pushViewController and popViewController methods.
FirstVC.navigationController?.popViewController(animated: animated) // pops the top view controller
self.navigationController?.pushViewController(SecondVC, animated: true) // Pushes a view controller onto navigation's stack of controllers
In your case:
let destinationController = self.storyboard?.instantiateViewController(withIdentifier: "goSecondVC") as? SecondVC
self.navigationController?.popViewController(animated: animated)
self.navigationController?.pushViewController(destinationController, animated: true)
I have the following code, which setups and shows a UIViewController to be embedded into a UINavigationController:
private func transitionToMainVC() {
let vc = UINavigationController(rootViewController: SpacesVC())
DispatchQueue.main.async {
self.show(vc, sender: self)
}
}
Now, inSpacesVC I want to show() another UIViewController, but outside of the "parent" UINavigationController.
When I use the following code in SpacesVC:
// Called by a button touch up inside
#objc private func transitionToMainVC() {
let vc = NextVC()
self.show(vc, sender: self)
}
It transitions to NextVC but it shows the navigation bar at the top of the screen; i.e. the new view controller is still embedded under the UINavigationController defines in the first snippet.
I am aware of the possibility of hiding the navigation bar in NextVC as such:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navController.isNavigationBarHidden = true
}
But I want to show() the NextVC without embedding it in the navigation controller, since I won't need that anymore. How can I do that?
Use this method instead of show.
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil)
How to present another view controller after dismiss from navigation controller in swift ?
I am using Navigation controller.
ViewController1.swift
func pushTo(viewController: UIViewController) {
let showLocalPwd = self.storyboard?.instantiateViewController(withIdentifier: "LocalPwdVC") as! LocalPwdVC
self.navigationController?.present(showLocalPwd, animated: true, completion: nil)
}
ViewController2.swift
#IBAction func btnVerify(_ sender: Any)
{
self.dismiss(animated: true, completion: {
let vc = self.storyboard.instantiateViewController(withIdentifier: "DataVC") as! DataVC
self.navigationController.pushViewController(vc, animated: true)
})
}
After dismissing the View Controller, it will not goes to next viewcontroller i.e. DataVC
If you want to present the view controller then create a protocol in your dismissViewController
protocol dismissViewController {
func presentCompletedViewController()
}
// Then create a delegate
var delegate = dismissViewController? = nil
// If you are using action to dismiss your ViewController then call delegate
#IBAction func YourButton(_ sender: Any) {
self.dismiss(animated: true) {
self.delegate!.presentCompletedViewController()
}
}
//And then call this in your main or homeViewController
class HomeViewController: UIViewController, dismissViewController {
func presentCompletedViewController(){
// enter code to present view controller
}
// And at last don't forget to call delegate
yourVc.delegate = self
you need to call this delegate in which you are presenting your dismissViewController
There are two ViewController in my app, ViewController and ViewController2
In ViewController, a button set Present Modally segue to "ViewController2"
And ViewController override viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("will appear")
}
In ViewController2, a button to go back
#IBAction func close(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
Now it still can trigger viewWillAppear then I go back to ViewController from ViewController2
If I change ViewController2's presentation from Full Screen to Over Current Context, viewWillAppear will not be triggered
How can I trigger some code when go back?
You can do it without giving up storyboard segues, but you nevertheless had to setup will/did Disappear handler in ViewCOntroller2:
class ViewController: UIViewController {
...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? ViewController2 {
(segue.destination as? ViewController2).onViewWillDisappear = {
//Your code
}
}
}
}
class ViewController2: UIViewController {
var onViewWillDisappear: (()->())?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onViewWillDisappear?()
}
...
}
There are several ways to handle this operation. Here is one, which I used to use.
// ViewController1
class ViewController1: UIViewController {
#IBAction func presentOverCurrentContext(button: Button) {
let vc2 = // instantiate ViewController2
vc2.modalPresentationStyle = .overFullScreen
vc2.presentingVC = self // use this variable 'presentingVC' to connect both view controllers
self.present(vc2, animated: true)
}
}
// ViewController2
class ViewController2: UIViewController {
var presentingVC: UIViewController? // use this variable to connect both view controllers
#IBAction func close(button: Button) {
// handle operation here
presentingVC?.viewWillAppear(true)
self.dismiss(animated: true, completion: {
// or here
// presentingVC?.viewWillAppear(true)
})
}
}
You can also use, your own method to reload view/viewcontroller, but viewWillAppear is common accessible method for all view controllers (as part of super class life cycle) hence you may not need to specify custom type of view controller for presentingVC
While the answers so far provided do work I think it's a good idea to show how to do it using a protocol and delegate as that's a clean implementation which then also allows for further functionality to be added with minimal effort.
So set up a protocol like this:
protocol SecondViewControllerProtocol: class {
func closed(controller: SecondViewController)
}
Setup the second view controller like this:
class SecondViewController {
public weak var delegate: SecondViewControllerProtocol?
#IBAction func close(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
self.delegate?.close(controller: self)
}
}
Setup the first view controller like this:
class FirstViewController: SecondViewControllerProtocol {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SecondViewControllerID",
let secondViewController = segue.destination as? SecondViewController {
secondViewController.delegate = self
}
}
func closed(controller: SecondViewController) {
// Any code you want to execute when the second view controller is dismissed
}
}
Implementing it like this does what the original request was and allows for extra methods to be put in the protocol so that the FirstViewController can respond to other actions in the SecondViewController.
Note:
You might want to move the delegate method call into the closure of the dismiss handler so that you know the method is not called until the SecondViewController is actually gone (in case you try to present another view which would fail). If that's the case you could do this:
#IBAction func close(_ sender: Any) {
self.dismiss(animated: true) {
self.delegate?.close(controller: self)
}
}
In fact you could have a will and did methods and call them like this:
#IBAction func close(_ sender: Any) {
self.delegate?.willClose(controller: self)
self.dismiss(animated: true) {
self.delegate?.didClose(controller: self)
}
}
Which would allow you to do something immediately while the second controller is animating away and then know when it has actually gone.
Best/Clean way to handle this scenario to use call back handler.
Example Code
typealias CloseActionHandler = ()-> Void
class TestController: UIViewController {
var closeActionHandler: CloseActionHandler?
func close(_ handler:#escaping CloseActionHandler) {
self.closeActionHandler = handler
}
#IBAction func closeButtonTapped(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
self.closeActionHandler?()
}
}
class ViewController: UIViewController {
func loadTestController(viewController: TestController) {
viewController.close {
//will be called when user will tap on close button
}
}
}