在iOS 13上检测表格被解散

170
在iOS 13之前,呈现的视图控制器通常会覆盖整个屏幕。当关闭时,父视图控制器的viewDidAppear函数将被执行。
现在,iOS 13默认将视图控制器呈现为一个卡片,这意味着该卡片将部分覆盖底层视图控制器,也就是说,viewDidAppear不会被调用,因为父视图控制器实际上从未消失。
有没有一种方法可以检测到已解除显示的视图控制器表格?我可以在父视图控制器中覆盖另一个函数,而不是使用某种代理吗?

6
非常感谢您的信任,请问需要翻译哪种语言?中文还是其他语言? - matt
那么有没有一种方法可以将所有模态表单同时关闭到根视图控制器? - Weslie
https://dev59.com/ElMI5IYBdhLWcg3wV6Jr - Varsha Vijayvargiya
你为什么需要知道它何时被解雇?如果是为了重新加载数据和更新UI,通知或KVO可能是一个不错的选择。 - martn_st
13个回答

97
有没有一种方法可以检测到已呈现的视图控制器表单已被解除?是的。使用某种代理来覆盖父视图控制器中的其他函数而不是使用某种代理?不。 “某种类型的代理”就是您要做的事情。将自己设置为呈现控制器的代理并覆盖presentationControllerDidDismiss(_ :)。

https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229889-presentationcontrollerdiddismiss


缺乏一般的运行时生成事件来通知您已呈现的视图控制器(无论全屏与否)已被解除,确实令人困扰;但这不是一个新问题,因为一直存在非全屏呈现的视图控制器。只是现在(在iOS 13中)它们更多了!我在其他地方专门提出了一个关于此主题的问题和答案:统一的UIViewController“成为最前面”检测?

9
这还不够。如果你的视图控制器中有一个 nabber 和一个自定义的导航栏按钮来编程性地关闭你的视图,那么 presentation controller did dismiss 不会被调用。 - Irina
31
嗨 @Irina - 如果您通过程序来关闭视图,则不需要回调,因为您是通过程序关闭视图的,您知道这是因为您自己操作了。委托方法只有在_用户_关闭视图时才使用。 - matt
10
谢谢你的回答。当视图被编程方式解除显示时,这个方法不会被调用(就像Irina所说的那样),你是正确的,我们知道我们已经完成了它。我只是认为为了获得一种类似于iOS 13中新的模态呈现样式的“viewWillAppear”,需要编写过多的样板代码。当您通过对路由进行管理的架构来提取路由(例如,在MVVM +协调器或VIPER中的路由器类型)时,情况变得特别混乱。 - Adam Waite
5
@AdamWaite 我同意,但这个问题并不是新的。我们已经遇到了多年,例如弹出窗口、非全屏呈现的视图控制器、警告等等。我认为这是苹果“事件”库中一个严重的缺陷。我只是在说实情以及原因。我在这里直接解决这个问题:https://dev59.com/RlQJ5IYBdhLWcg3wFx1_ - matt
1
presentationControllerDidDismiss(_:). 当我在子视图控制器中点击返回按钮时没有被调用。有什么帮助吗? - Krishna Meena
显示剩余2条评论

61

下面是一个父视图控制器的代码示例,当它呈现的子视图控制器以 模式窗口(即在默认的iOS 13方式中)被解除时,父视图控制器会收到通知:

这里是一个父级视图控制器的代码示例,当它呈现的子视图控制器以表单形式(即在默认的iOS 13方式中)被关闭时,会向父视图控制器发送通知:

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的回答有些混淆,因为(a)原问题提出者想要在表单被解散时调用函数(而他实现了presentationControllerDidAttemptToDismiss,这个函数是在用户尝试但失败地关闭表单时调用的),以及(b)设置isModalInPresentation完全正交,事实上会使呈现的表单无法关闭(这与OP的愿望相反)。


8
这很有效。只是一个提示,如果在被调用的视图控制器上使用了导航控制器,你应该将导航控制器指定为presentationController?的代理(而不是导航控制器顶部视图控制器所拥有的视图控制器)。 - instAustralia
@instAustralia,你能解释为什么吗?或者给出一个参考文档?谢谢。 - Ahmed Osama
presentationControllerDidDismiss 如何在用户按下返回按钮时调用它? - Krishna Meena
@AhmedOsama - 导航控制器是表示控制器,因此它是委托,因为它将是响应解除的那个。我也尝试了嵌入在Nav Controller中的VC,但这是我的实际按钮存在和响应的地方。我在苹果文档中找不到它,但在这里有引用https://sarunw.com/posts/modality-changes-in-ios13/。 - instAustralia

31

以下是更完整的答案和实现方式,供后来者参考:

  1. 在根视图控制器的“prepare for segue”方法中添加以下内容(假设你的模态视图有一个导航控制器)
    // Modal Dismiss iOS 13
    modalNavController.presentationController?.delegate = modalVc
  1. 在模态视图控制器中添加以下委托和方法
// 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)
    }
}
确保在模态视图控制器中,以下属性为true,以便调用委托方法。
    self.isModalInPresentation = true
  • Profit(利润)

2
self.isModalInPresentation = true时,拖动dismiss无法工作。删除该行后,委托方法仍然会被调用。好的,谢谢。 - Yogesh Patel
4
由于以下两个原因,这让人感到困惑:(a) 原问题提问者希望在表单关闭时获得一个函数调用(而您已经实现了presentationControllerDidAttemptToDismiss,它会在用户尝试并失败地关闭表单时被调用),以及 (b) 设置isModalInPresentation是完全独立的操作,事实上会使呈现的表单无法关闭(这与OP想要的相反)。 - SuddenMoustache
1
跟进@Matt的回答点(a):使用 presentationControllerDidDismiss 应该可以解决问题。 - gondo
1
不完全正确,因为presentationControllerDidAttemptToDismiss是用于当用户试图以编程方式阻止时(仔细阅读该方法的文档)而预期的情况。presentationControllerWillDismiss方法用于检测用户打算解除OR presentationControllerShouldDismiss来控制解除OR presentationControllerDidDismiss来检测已被解除的事实。 - Vitalii

31

另一个获取 viewWillAppearviewDidAppear 的选项是设置

let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen

这个选项可以覆盖整个屏幕,关闭后会调用上述方法。


2
谢谢PiterPan。这个可行。这是最好和最快的解决方案。 - Erkam KUCET
感谢您提供这种快速可靠的方式来恢复以前的默认行为。能够立即实施此修复措施,然后合理地计划过渡到新行为,这真是太好了。 - Ian Lovejoy
21
这只是一种解决方法,而不是真正的修复。让每个人都返回iOS 12风格的样式表并不是最好的选择。iOS 13的样式表很酷! :) - SuddenMoustache
1
使用此代码时请注意,iPad 在以模态方式呈现时默认为页面表单。这将强制 iPad 以全屏方式呈现。 - wyu
不起作用。我打开模态控制器,使用dismiss关闭它,但是willAppear没有被调用。为什么?谢谢。 - nonickname

10

如果您希望在用户从模态表单中关闭时执行某些操作,假设您已经拥有一个带有 @IBAction 的关闭按钮,并且具有在关闭之前显示警告或执行其他操作的逻辑。您只需要检测到用户何时按下此控制器。

方法如下:

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

4
如果您的模态视图控制器嵌入了导航控制器,则可能需要调用 self.navigationController?.presentationController?.delegate = self - Oluwatobi Omotayo

8

Swift

iOS13中调用viewWillAppear的通用解决方案

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

3
只有通过从顶部滑动来关闭窗口,才能使用此功能。不能通过调用函数 dismiss(_) 来关闭窗口。 - Pedro Paulo Amorim

7

重写即将消失的UIViewController上的viewWillDisappear方法。通过isBeingDismissed布尔标志,它将提醒您该视图控制器正在被消除。

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

    if isBeingDismissed {
        print("user is dismissing the vc")
    }
}

如果用户在向下滑动时只完成一半的距离,然后将卡片向上滑回去,即使未将该卡片解除,也会被注册为已解除。但这是一个边界情况,您可能不太关心。


self.dismiss(animated: Bool, completion: (() -> Void)?) 是什么意思? - iGhost
self.dismiss(animated: Bool, completion: (() -> Void)?) 不会检测到解除显示。相反,它将导致一个操作发生,然后您可以在其上进行一些工作。使用 viewWillDisappear 将监听解除显示的事件。 - craft

4

拖拽或调用DISMISS函数将使用以下代码:

1)在根视图控制器中,您可以通过以下代码指定其表示视图控制器:

 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) 再次在根视图控制器中,您告诉它的呈现视图控制器被解除时会发生什么

public func presentationControllerDidDismiss(
  _ presentationController: UIPresentationController)
{
    print("presentationControllerDidDismiss")
}

1) 在演示视图控制器中,当您点击此图片中的取消或保存按钮时,将调用以下代码。

self.dismiss(animated: true) {
        self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
    }

enter image description here


1
需要将 navigationController.topViewController 转换为 presentationViewController 吗?我发现不需要这样做。 - Fitsyu
在子视图控制器上点击取消按钮后,如何重新加载父视图控制器中的数据? - Krishna Meena

2
在SwiftUI中,您可以使用onDismiss闭包。
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View

1
如果某人无法访问所呈现的视图控制器,他们可以在呈现视图控制器中覆盖以下方法,并将 modalPresentationStyle 更改为 fullScreen 或者使用上述方法之一添加策略。
 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 let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
   viewControllerToPresent.modalPresentationStyle = .fullScreen
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接