检测表在 iOS 13 上被取消

Posted

技术标签:

【中文标题】检测表在 iOS 13 上被取消【英文标题】:Detecting sheet was dismissed on iOS 13 【发布时间】:2019-10-27 08:49:00 【问题描述】:

ios 13 之前,呈现用于覆盖整个屏幕的视图控制器。并且,当关闭时,父视图控制器viewDidAppear 函数被执行。

现在 iOS 13 将默认将视图控制器呈现为工作表,这意味着卡片将部分覆盖底层视图控制器,这意味着 viewDidAppear 将不会被调用,因为父视图控制器从未真正消失过。

有没有办法检测呈现的视图控制器工作表已被解除?我可以在父视图控制器中覆盖其他一些功能而不是使用某种委托

【问题讨论】:

在developer.apple.com/videos/play/wwdc2019/224中讨论得很好 那么有没有办法一次性将所有模态表解散到根vc? ***.com/questions/56435510/… 为什么需要知道它何时被解雇?如果要重新加载数据和更新 UI,Notifications 或 KVO 可能是一个不错的选择。 【参考方案1】:

有没有办法检测呈现的视图控制器工作表已被解除?

是的。

我可以在父视图控制器中覆盖其他一些功能,而不是使用某种委托?

没有。 “某种代表”就是你的做法。让自己成为演示控制器的代表并覆盖 presentationControllerDidDismiss(_:)

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


缺少一个通用的运行时生成的事件来通知您呈现的视图控制器(无论是否全屏)已被解除,这确实很麻烦;但这不是一个新问题,因为一直存在非全屏呈现的视图控制器。只是现在(在 iOS 13 中)还有更多!我在别处专门针对此主题单独问答:Unified UIViewController "became frontmost" detection?。

【讨论】:

这还不够。如果您在呈现的 VC 中有一个 nabber 和一个以编程方式关闭您的视图的自定义栏按钮,则表示控制器确实关闭不会被调用。 嗨@Irina - 如果您以编程方式关闭视图,则不需要回调,因为您以编程方式关闭视图 - 你知道你这样做是因为 做到了。委托方法仅在 user 这样做的情况下使用。 @matt 感谢您的回答。当视图以编程方式被关闭时,它不会被调用(正如 Irina 所说),你是对的,我们知道我们做到了。我只是认为,为了在 iOS13 中使用新的模态演示样式获得一种“viewWillAppear”,编写了不必要的样板代码。当您通过提取路由的架构(例如在 MVVM + 协调器中,或 VIPER 中的路由器类型)管理路由时,它会变得特别混乱 @AdamWaite 我同意,但这个问题并不新鲜。多年来,我们一直遇到这个问题,弹出窗口、非全屏显示的视图控制器、警报等等。我认为这是苹果“事件”目录中的一个严重缺陷。我只是说现实是什么以及为什么。我在这里直接解决这个问题:***.com/questions/54602662/… presentationControllerDidDismiss(_:)。当我单击子 VC 中的后退按钮时未调用。有什么帮助吗?【参考方案2】:

这是一个父视图控制器的代码示例,当它作为工作表呈现的子视图控制器(即以默认的 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) 最初的提问者希望在工作表dismissed 时获得函数调用(而他实现了presentationControllerDidAttemptToDismiss,当用户尝试和未能关闭工作表),并且(b)设置 isModalInPresentation 是完全正交的,实际上会使呈现的工作表不可关闭(这与 OP 想要的相反)。

【讨论】:

这很好用。只是一个提示,如果您在调用的 VC 上使用导航控制器,您应该将导航控制器分配为presentationController?,委托(而不是导航具有作为 topViewController 的 VC)。 @instAustralia 您能否解释原因或参考文档?谢谢。 presentationControllerDidDismiss 如何在用户按下返回按钮时调用它? @AhmedOsama - 导航控制器是表示控制器,因此是代表,因为它将是响应解雇​​的人。我也尝试过嵌入在导航控制器中的 VC,但这是我要关闭的实际按钮存在并做出响应的地方。我无法直接在 Apple 文档中找到它,但这里引用了它 sarunw.com/posts/modality-changes-in-ios13【参考方案3】:

设置了返回viewWillAppearviewDidAppear 的另一个选项

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

此选项覆盖全屏,关闭后调用上述方法

【讨论】:

谢谢 PiterPan。这是有效的。这是一个很棒且最快的解决方案。 感谢您以这种快速可靠的方式恢复以前的默认行为。能够立即进行此修复,然后以合理的方式计划向新行为的过渡,真是太好了。 这是一种解决方法而不是修复方法。只是恢复到 iOS 12 样式表对每个人来说都不是很好。 iOS 13 很酷! :) 在 iPad 上使用它时要小心,因为 iPad 在模态显示时默认显示为 pageSheet。这将强制 iPad 显示为全屏 不适合我。我打开模态控制器。用解雇关闭它,但不会调用 willAppear 。为什么?谢谢【参考方案4】:

对于未来的读者,这里有一个更完整的实施答案:

    在根视图控制器准备 segue 添加以下内容(假设您的模态有一个导航控制器)
    // Modal Dismiss iOS 13
    modalNavController.presentationController?.delegate = modalVc
    在模态视图控制器中添加以下委托 + 方法
// 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)
    

    确保在模态视图控制器中以下属性为真,以便调用委托方法
    self.isModalInPresentation = true
    利润

【讨论】:

self.isModalInPresentation = true 然后拖动关闭不起作用。删除该行委托方法仍然可以调用。谢谢。 这很困惑,因为 (a) 原始提问者希望在关闭工作表时获得函数调用(而您已经实现了presentationControllerDidAttemptToDismiss,当用户尝试关闭工作表但失败时调用它) 和 (b) 设置 isModalInPresentation 是完全正交的,实际上会使呈现的工作表不可关闭(这与 OP 想要的相反)。 跟进@Matt 的回答点(a):使用presentationControllerDidDismiss 应该可以工作 不太正确,因为presentationControllerDidAttemptToDismiss 适用于用户尝试关闭但被编程阻止的情况(请仔细阅读该方法的文档)。 presentationControllerWillDismiss 方法是检测用户的解雇意图或presentationControllerShouldDismiss 控制解雇或presentationControllerDidDismiss 检测被解雇事实的方法【参考方案5】:

斯威夫特

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)
            
        
    

【讨论】:

这仅处理使用从顶部滑动的解除,而不是通过调用函数dismiss(_)【参考方案6】:

在被解除的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)?)怎么样 self.dismiss(animated: Bool, completion: (() -> Void)?) 不会检测到解雇。相反,它会导致一个动作发生,然后你就捎带它去做一些工作。使用viewWillDisappear 将监听解雇事件。【参考方案7】:

如果您想在用户从该工作表中关闭模式工作表时执行某些操作。 假设您已经有一些带有@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
    

【讨论】:

如果您的模态视图控制器嵌入在导航控制器中,您可能需要调用self.navigationController?.presentationController?.delegate = self【参考方案8】:

DRAG OR CALL DISMISS FUNC 将使用以下代码。

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

【讨论】:

是否需要将navigationController.topViewController 转换为presentationViewController?我发现不是 从取消按钮子 VC 中解散后,如何在父 VC 中重新加载数据?【参考方案9】:

在 SwiftUI 中你可以使用 onDismiss 闭包

func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View

【讨论】:

【参考方案10】:

如果有人无权访问呈现的视图控制器,他们可以在呈现视图控制器中覆盖以下方法并将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

【讨论】:

【参考方案11】:

如果您在 FullScreen 中使用 ModalPresentationStyle,则控制器的行为将恢复正常。

ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController; controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; this.NavigationController.PushViewController(controllerConsultar, true);

【讨论】:

重复现有答案。【参考方案12】:

在我看来,苹果不应该将pageSheet设置为默认modalPresentationStyle

我想使用 swizzlingfullScreen 样式恢复为默认样式

像这样:

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)
    

然后把这行写到你的AppDelegate

UIViewController.preventPageSheetPresentationStyle()

【讨论】:

这很巧妙,但我不能同意。它很老套,更重要的是,它违背了 iOS 13 的特点。您应该在 iOS 13 中使用“卡片”演示文稿。Apple 对我们的期望不是“解决它” ";这是“克服它”。 同意你的观点,这个解决方案无助于使用 Apple 鼓励我们的卡片展示风格。但是,将其设置为默认样式会使现有代码行在某处出错,因为presentingViewController 不会触发viewWillAppear 是的,但正如我在自己的回答中已经说过的那样,对于非全屏演示(例如 iPad 上的弹出框和页面/表单)来说,始终是一个问题,所以这不是什么新鲜事。只是现在还有更多。依赖viewWillAppear某种意义上 总是错误的。当然,我不喜欢 Apple 出现并从我身下挖出地板。但正如我所说,我们只需要接受它并以新的方式做事。 在我的项目中,有一些场景我不知道视图控制器(称为presentedController)出现在哪里,也不知道presentingViewController 到底是什么。例如:在某些情况下,我必须使用UIViewController.topMostViewController(),它会返回当前窗口上最顶层的视图控制器。这就是为什么我想在我的视图控制器的viewWillAppear 中保持当前行为以做正确的事情(刷新数据、UI)。如果您对解决该问题有任何想法,请提供帮助。 好吧,我相信我在答案末尾链接到的解决方案确实可以解决这个问题。在演示时进行配置需要一些工作,但基本上它可以保证每个演示者(包括警报演示者)在所呈现的视图控制器被解除时听到。【参考方案13】:

调用presentingViewController.viewWillAppear不是很简单吗? 解雇前?

self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)

【讨论】:

这不是你的电话。

以上是关于检测表在 iOS 13 上被取消的主要内容,如果未能解决你的问题,请参考以下文章

如何在ios中检测系统弹出视图的“取消”按钮?

UISearchController 中的取消按钮在 iOS 13 中没有正确消失

使用 Javascript 检测已取消的表单帖子

Xamarin 表单选择器确定或取消按钮按下检测

在 iOS 7 中取消 UITableViewCell 滑动删除时收到通知

MFMailComposeViewController 操作表在 iOS5 上 setStatusBarHidden:YES 时不在屏幕上