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 one View Controller and this View Controller contains two views/scenes in the main.storybard.
I am trying to hide the top navigation bar at first view/scene, but unhide it again on the second view/scene.
I tried with
self.navigationController?.isNavigationBarHidden = true
But this will only work with two View Controller classes.
Does anyone have a idea to manage it?
Hide navigationbar in viewWillAppear & unhide in viewWillDisappear
var shouldHideNavBar = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(shouldHideNavBar, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if shouldHideNavBar == true {
navigationController?.setNavigationBarHidden(false, animated: animated)
}
}
And when you perform segue set shouldHideNavBar as true
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "show") {
let viewController = segue!.destinationViewController as! ViewController
viewController.shouldHideNavBar = true
}
}
You should use extra control. Or you can create an IBInspactable variable and assing it's value on Interface Builder. Like this:
#IBDesignable class myViewController: UIViewController{
#IBInspectable var isNavbarHidden: Bool = true{
didSet{
self.navigationController?.isNavigationBarHidden = isNavBarHidden
}
}
override func viewDidLoad(){
super.viewDidLoad()
//I am not sure if this line is necessary
self.navigationController?.isNavigationBarHidden = isNavBarHidden
}
}
After then go to InterfaceBuilder(your storyboard file) and set its value for your Scenes on your viewControllers properties.
I have a UITabBarController. One of the tabs contains a UINavigationController.
I'd like to push a view controller onto the navigation stack and hide the tab bar on that view controller. I can do this easily with:
toVC.tabBarController?.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(toVC, animated: true)
or doing it in the storyboard:
The problem is, this hides the tab bar for any subsequent view controllers I push onto the stack. I'd like to simply hide the tab bar for this one view controller and show it for all other view controllers before and after it.
There is a workaround. It works the way it is presented on gif below.
For each UIViewController that is pushed into the UINavigationController stack I override the hidesBottomBarWhenPushed property this way:
override var hidesBottomBarWhenPushed: Bool {
get {
switch navigationController?.topViewController {
case .some(let controller):
switch controller == self {
case true:
return super.hidesBottomBarWhenPushed
case false:
return false
}
default:
return super.hidesBottomBarWhenPushed
}
}
set {
super.hidesBottomBarWhenPushed = newValue
}
}
The first switch checks whether this controller belongs to some UINavigationController stack. The second switch checks whether current top UIViewController of UINavigationController stack is self.
Hope it will work in your case. Happy coding (^
If you hide on the storyboard then by this property your tab bar will hide for all the view controllers. So you can manage this by code.
You can do this programmatically by just writing one line of code in ViewDidLoad() or ViewWillAppear() method
For Swift 3:-
self.tabBarController?.tabBar.isHidden = true
And where you want to unhide the tab bar just write the following code in ViewDidLoad () or ViewWillAppear() method
self.tabBarController?.tabBar.isHidden = false
Try this in the view controller you want to hide the tab bar in:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = true
}
And this in the view controllers before and after the one you want to hide the tab bar in:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
}
EDIT:
Fully implemented example:
class ViewController1: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
}
}
class ViewController2: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = true
}
}
class ViewController3: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
}
}
In my rootViewController i have hidden status bar. When i tried to push the new viewcontroller and pop back, then navigation bar is pushed up and there is the gap between navigation bar and content view. Here are the screenshot of navigation bars.
Here is the code in rootViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.statusBarHidden = true
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
private var statusBarHidden = true {
didSet {
self.setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
get {
return statusBarHidden
}
}
I resolved this issue by adding the following line to -viewDidLoad:
extendedLayoutIncludesOpaqueBars = true
I have 3 view controllers which are added to a pageViewController so I can scroll between the 3. The issue is I want to display the status bar in only 1 of the viewControllers. So far I can hide from them all or show in them all.
I tried the following:
private var isStatusBarHidden = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
return isStatusBarHidden
}
How I added the VC's as child view controllers to my scroll view:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
page1 = storyboard.instantiateViewController(withIdentifier: StoryboardIdentifiers.feedViewController.rawValue) as! FeedViewController
page1.view.translatesAutoresizingMaskIntoConstraints = false
page1.delegate = self
scrollView.addSubview(page1.view)
addChildViewController(page1)
page1.didMove(toParentViewController: self)
You have 3 VC means all 3 ViewController is going to have viewDidAppear and viewWillDisappear code
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
//It will show the status bar again after dismiss
UIApplication.shared.isStatusBarHidden = true
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//It will hide the status bar again after dismiss
UIApplication.shared.isStatusBarHidden = false
}
override open var prefersStatusBarHidden: Bool {
return true
}
Copy and paste code into those ViewController in which you want to hide your status bar.
So what it will do is inside your viewDidAppear it will hide your status bar
and as soon as we will leave the class it will set status bar visible.
In case your pageViewController is parent view then We can do it via page index
Let suppose you want to show status bar on page 2 and hide on page 1 and 3.
So we can do it, in this page
PageDataSource Function {
if(index == 1 || index == 3){
UIApplication.shared.isStatusBarHidden = true
}
else{
UIApplication.shared.isStatusBarHidden = false
}
}
override open var prefersStatusBarHidden: Bool {
return true
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//It will hide the status bar again after dismiss
UIApplication.shared.isStatusBarHidden = false
}
Please try this and let me know if its working or not
Thank you