【问题标题】:Detect when a presented view controller is dismissed检测呈现的视图控制器何时被解除
【发布时间】:2015-12-27 11:29:49
【问题描述】:

假设我有一个名为 VC2 的视图控制器类的实例。在 VC2 中,有一个“取消”按钮会自行关闭。但是当“取消”按钮被触发时,我无法检测或接收任何回调。 VC2 是一个黑盒子。

视图控制器(称为 VC1)将使用 presentViewController:animated:completion: 方法呈现 VC2。

当 VC2 被解除时,VC1 必须检测哪些选项?

编辑:根据@rory mckinnel 的评论和@NicolasMiari 的回答,我尝试了以下方法:

在 VC2 中:

-(void)cancelButton:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:^{

    }];
//    [super dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

在 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:^{
//        
//    }];
}

但是 VC1 中的 dismissViewControllerAnimated 没有被调用。

【问题讨论】:

  • 在VC1中会调用viewWillAppear方法
  • 根据文档,呈现控制器负责实际解雇。当被展示的控制器关闭自己时,它会要求展示者为它做这件事。因此,如果您在 VC1 控制器中覆盖 dismissViewControllerAnimated,我相信当您在 VC2 上点击取消时它会被调用。检测解除,然后调用将执行实际解除的超类版本。
  • 您可以通过调用[self.presentingViewController dismissViewControllerAnimated] 来测试您的覆盖。可能是内部代码有不同的机制来要求演示者进行解雇。
  • @RoryMcKinnel:使用 self.presentingViewController 在我的实验室 VC2 以及真正的黑匣子中确实有效。如果您将 cmets 放入答案中,那么我将选择它作为答案。谢谢。
  • 可以在相关帖子中找到解决方案:stackoverflow.com/a/34571641/3643020

标签: ios uiviewcontroller


【解决方案1】:
  1. 创建一个类文件(.h/.m)并将其命名为:DismissSegue
  2. 选择子类:UIStoryboardSegue

  3. 转到 DismissSegue.m 文件并写下以下代码:

    - (void)perform {
        UIViewController *sourceViewController = self.sourceViewController;
        [sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
    
  4. 打开故事板,然后按住 Ctrl 键并从取消按钮拖动到 VC1 并选择 Action Segue 作为 Dismiss,您就完成了。

【讨论】:

    【解决方案2】:

    呈现的呈现的视图控制器都可以调用dismissViewController:animated: 来关闭呈现的视图控制器。

    前一个选项(可以说)是“正确”的选项,设计方面:同一个“父”视图控制器负责呈现和关闭模态(“子”)视图控制器。

    但是,后者更方便:通常,“关闭”按钮附加到呈现的视图控制器的视图上,并且它已将视图控制器设置为其操作目标。

    如果您采用前一种方法,您已经知道呈现视图控制器中发生解除的代码行:或者在 dismissViewControllerAnimated:completion: 之后运行您的代码,或者在完成块内运行。

    如果您采用后一种方法(呈现的视图控制器自行关闭),请记住,从呈现的视图控制器调用 dismissViewControllerAnimated:completion: 会导致 UIKit 依次在呈现的视图控制器上调用该方法:

    讨论

    呈现视图控制器负责 关闭它呈现的视图控制器。如果你调用这个方法 在呈现的视图控制器本身上,UIKit 询问呈现 视图控制器来处理解雇。

    (source: UIViewController Class Reference)

    因此,为了拦截此类事件,您可以在 presenting 视图控制器中重写该方法:

    override func dismiss(animated flag: Bool,
                             completion: (() -> Void)?) {
        super.dismiss(animated: flag, completion: completion)
    
        // Your custom code here...
    }
    

    【讨论】:

    • 没问题。但事实证明它没有按预期工作。值得庆幸的是,@RoryMcKinnel 的回答似乎提供了更多选择。
    • 虽然这种方法足够通用,可以从基础视图控制器广告中对视图控制器进行子类化,并覆盖其中的dismissViewControllerAnimated。但如果您尝试在导航视图控制器中包装视图控制器,它会失败
    • 当用户从顶部滑动关闭模态视图控制器时不会调用它!
    【解决方案3】:

    根据文档,呈现控制器负责实际解雇。当被展示的控制器关闭自己时,它会要求展示者为它做这件事。因此,如果您在 VC1 控制器中覆盖dismissViewControllerAnimated,我相信当您在VC2 上点击取消时它会被调用。检测解除,然后调用将执行实际解除的超类版本。

    从讨论中发现,这似乎不起作用。与其依赖底层机制,不如在VC2本身上调用dismissViewControllerAnimated:completion,而是在VC2中对self.presentingViewController调用dismissViewControllerAnimated:completion。然后这将直接调用您的覆盖。

    更好的方法是让 VC2 提供一个在模态控制器完成时调用的块。

    所以在 VC2 中,提供一个名为 onDoneBlock 的块属性。

    在 VC1 中你呈现如下:

    • 在VC1中,创建VC2

    • 将 VC2 的完成处理程序设置为:VC2.onDoneBlock={[VC2 dismissViewControllerAnimated:YES completion:nil]};

    • 使用 [self presentViewController:VC2 animated:YES completion:nil] 正常展示 VC2 控制器;

    • 在VC2中,在取消目标动作调用self.onDoneBlock();

    结果是 VC2 告诉提出它的人它已经完成。您可以扩展 onDoneBlock 以具有指示模态是否完成、取消、成功等参数的参数......

    【讨论】:

    • 只是想感谢并欣赏它的工作原理……即使在 4 年后!谢谢!
    • 我正在经历这个。如果我关闭呈现控制器 VC1 中的控制器,则永远不会调用 VC2 deinit。可能会导致保留周期,因为关闭 onDoneBlock 并不弱。因此,删除块中解除 V2 控制器的行并调用 VC2 中的块,并在同一 VC2 中的下一行中,解除 VC2 中的控制器本身即可解决问题。这确保了我能够检测到所提供的控制器何时被调用的块解散,并且还调用了 VC2 的 deinit。
    【解决方案4】:

    你可以使用 unwind segue 来完成这个任务,不需要使用dismissModalViewController。在你的 VC1 中定义一个 unwind segue 方法。

    请参阅此链接了解如何创建展开转场,https://stackoverflow.com/a/15839298/5647055

    假设您的展开转场已设置,在为“取消”按钮定义的操作方法中,您可以将转场执行为 -

    [self performSegueWithIdentifier:@"YourUnwindSegueName" sender:nil];
    

    现在,每当您在 VC2 中按下“取消”按钮时,它都会被关闭并出现 VC1。它还将调用您在 VC1 中定义的 unwind 方法。现在,您知道呈现的视图控制器何时被关闭。

    【讨论】:

      【解决方案5】:

      @user523234 - “但是 VC1 中的 dismissViewControllerAnimated 没有被调用。”

      您不能假设 VC1 确实进行了呈现 - 例如,它可能是根视图控制器 VC0。涉及 3 个视图控制器:

      • sourceViewController
      • 呈现ViewController
      • 呈现的ViewController

      在您的示例中,VC1 = sourceViewControllerVC2 = presentedViewController?? = presentingViewController - 也许是 VC1,也许不是。

      但是,在关闭 VC2 时,您始终可以依赖 VC1.animationControllerForDismissedController 被调用(如果您已实现委托方法),并且在该方法中您可以使用 VC1 做您想做的事情

      【讨论】:

        【解决方案6】:

        如果您覆盖正在关闭的视图控制器:

        override func removeFromParentViewController() {
            super.removeFromParentViewController()
            // your code here
        }
        

        至少这对我有用。

        【讨论】:

        • @JohnScalo 不正确,很多“本机”视图控制器层次结构使用子/父原语实现自己。
        【解决方案7】:
        【解决方案8】:

        使用块属性

        在 VC2 中声明

        var onDoneBlock : ((Bool) -> Void)?
        

        VC1 中的设置

        VC2.onDoneBlock = { result in
            // Do something
        }
        

        当你即将解散时调用 VC2

        onDoneBlock!(true)
        

        【讨论】:

        • @Bryce64 这对我不起作用,我得到“线程 1:致命错误:在展开可选值时意外发现 nil”,代码转到 onDoneBlock!(真)跨度>
        • @Lucas 听起来您没有在 VC1 中正确声明它。这 ”!”如果您没有正确设置它,则强制展开以强制出错。
        • 假设只有一个视图控制器被呈现。你可能在导航堆栈上,天知道在哪里。
        • @LeeProbert 没错。我们有一个提供的导航控制器,它的堆栈中有大约 10 个可能的子控制器,几乎每个子控制器都可以触发解雇......在这种情况下,任何完成块都必须传递给所有 10 个这样的控制器
        【解决方案9】:

        我使用以下命令向协调器发出视图控制器“完成”的信号。这在 tvOS 应用程序的 AVPlayerViewController 子类中使用,将在 playerVC 解除转换完成后调用:

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

        【讨论】:

        • 你不应该从 AVPLayerViewController 继承。 Apple 文档说:“不支持子类化 AVPlayerViewController 并覆盖其方法,这会导致未定义的行为。”
        【解决方案10】:

        overrideing viewDidAppear 帮了我大忙。我在我的模态中使用了Singleton,现在可以在调用的 VC、模态和其他任何地方设置和获取它。

        【讨论】:

        • viewDidAppear?
        • 您是说 viewDidDisappear 吗?
        【解决方案11】:

        如前所述,解决方案是使用override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil)

        对于那些想知道为什么override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) 似乎并不总是有效的人,您可能会发现呼叫被UINavigationController 拦截(如果它正在被管理)。我写了一个应该有帮助的子类:

        class DismissingNavigationController: UINavigationController { override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { super.dismiss(animated: flag, completion: completion) topViewController?.dismiss(animated: flag, completion: completion) } }

        【讨论】:

          【解决方案12】:

          在处理这个问题时,我已经看过这个帖子很多次了,我想我终于可以对可能的答案有所了解了。

          如果您需要了解用户启动的操作(如屏幕上的手势)是否对 UIActionController 进行了解雇,并且不想花时间在代码中创建子类或扩展或其他任何内容,那么有另一种选择。

          事实证明,UIActionController(或者更确切地说,任何 UIViewController)的 popoverPresentationController 属性有一个 delegate,您可以在代码中随时设置,其类型为 UIPopoverPresentationControllerDelegate ,并有以下方法:

          从您的操作控制器分配委托,在委托类(视图、视图控制器或其他)中实现您选择的方法,瞧!

          希望这会有所帮助。

          【讨论】:

          • 自 iOS 13 起已弃用。Doh
          • 没有嘶嘶声,没有嘶嘶声。但仅适用于 iPad。似乎除了继承 UIActivityViewController 之外别无他法。 (这个想法一点也不坏;你的答案几乎得到了赞成)
          【解决方案13】:

          以下列方式使用willMove(toParent: UIViewController?) 似乎对我有用。 (在 iOS12 上测试)。

          override func willMove(toParent parent: UIViewController?) {
              super.willMove(toParent: parent);
          
              if parent == nil
              {
                  // View controller is being removed.
                  // Perform onDismiss action
              }
          }
          

          【讨论】:

          • 奇怪的是,这也是唯一对我有用的解决方案。
          • 只有在容器 VC 中添加或删除时才有效。
          【解决方案14】:

          UIViewController 中有一个特殊的布尔属性,称为 isBeingDismissed,您可以将其用于此目的:

          override func viewWillDisappear(_ animated: Bool) {
              super.viewWillDisappear(animated)
              if isBeingDismissed {
                  // TODO: Do your stuff here.
              }
          }
          

          【讨论】:

          • 最简单的最佳答案,正确解决大部分问题,不需要额外的实现。
          • 如果不与viewDidAppear配对,它将无法正常工作。
          • 在 iOS13 模态演示中,当用户开始拖动控制器以关闭时,这是正确的,但他们可以选择不完成关闭。
          • viewDidDisappear 是比较合适的方法
          • 代替“viewWillDisappear”,您可以在“viewDidDisappear”中使用“isBeingDismissed”。覆盖 func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isBeingDismissed { print("DISSMISSED!") } }
          【解决方案15】:

          如果你想处理视图控制器的关闭,你应该使用下面的代码。

          - (void)viewWillDisappear:(BOOL)animated {
              [super viewWillDisappear:animated];
              if (self.isBeingDismissed && self.completion != NULL) {
                  self.completion();
              }
          }
          

          很遗憾,我们无法在覆盖的方法中调用完成 - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion;,因为只有在调用此视图控制器的解除方法时才会调用此方法。

          【讨论】:

          • 但是如果不与viewDidAppear 配对,viewWillDisappear 也无法正常工作。
          • viewWillDisappear 在 VC 被完全覆盖时调用(例如,使用模态)。你可能没有被解雇
          【解决方案16】:

          另一种选择是收听自定义 UIPresentationController 的dismissalTransitionDidEnd()

          【讨论】:

            【解决方案17】:
            extension Foo: UIAdaptivePresentationControllerDelegate {
                func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
                    //call whatever you want
                }
            }
            
            vc.presentationController?.delegate = foo
            

            【讨论】:

            • iOS 13.0+ 仅限
            • 如果以编程方式完成解雇,也不会调用
            • 非常适合我的用例。当用户通过手动向下滑动视图“取消”时触发它
            【解决方案18】:

            我已将 deinit 用于 ViewController

            deinit {
                dataSource.stopUpdates()
            }
            

            在释放类实例之前立即调用去初始化器。

            【讨论】:

              【解决方案19】:

              我没有看到似乎是一个简单的答案。如果这是重复,请原谅我......

              由于 VC1 负责解散 VC2,因此您需要在某个时候调用 vc1.dismiss()。所以你可以在 VC1 中重写 dismiss() 并将你的操作代码放在那里:

              class VC1 : UIViewController {
                  override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
                      super.dismiss(animated: flag, completion: completion)
                      // PLACE YOUR ACTION CODE HERE
                  }
              }
              

              编辑: 您可能希望在关闭完成时触发您的代码,而不是在它开始时触发。所以在这种情况下,你应该使用:

                  override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
                      super.dismiss(animated: flag) {
                          if let unwrapCompletion = completion { unwrapCompletion() }
                          // PLACE YOUR ACTION HERE
                      }
                  }
              

              【讨论】:

                【解决方案20】:

                一种更高效的方法是创建一个用于呈现控制器的协议,然后调用 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)
                    }
                }
                

                然后当视图即将关闭时:

                self.dismissWithListener(animated: true, completion: nil)
                

                最后只需将协议添加到您希望收听的任何 viewController!

                class ViewController: UIViewController, DismissListener {
                
                    func childControllerWillDismiss(_ controller: UIViewController, animated: Bool) {
                    }
                    
                    func childControllerDidDismiss(_ controller: UIViewController, animated: Bool) {
                    }
                }
                

                【讨论】:

                  【解决方案21】:

                  你可以在父视图控制器上使用UIViewControllerTransitioningDelegate来观察另一个呈现的视图控制器的消失:

                  anotherViewControllerYouWantToObserve.transitioningDelegate = self
                  

                  并观察解雇:

                  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
                      print("anotherViewControllerYouWantToObserve was dismissed")
                      return nil
                  }
                  

                  【讨论】:

                  • 我相信要采用的正确代表是 UIViewControllerTransitioningDelegate 来观察 forDismissed
                  • 效果很好,是的,但我相信您需要在答案中将UIAdaptivePresentationControllerDelegate 更改为UIViewControllerTransitioningDelegate
                  • 我已经编辑了答案,使其显示UIViewControllerTransitioningDelegate
                  【解决方案22】:

                  如果您有一个可以像页面表一样通过滑动来关闭的模态演示,这很有效。

                  override func endAppearanceTransition() {
                              if isBeingDismissed{
                                  print("dismissal logic here")
                              }
                   }
                  

                  【讨论】:

                    猜你喜欢
                    • 2015-04-10
                    • 1970-01-01
                    • 2018-06-10
                    • 2021-11-12
                    • 1970-01-01
                    • 2017-04-07
                    • 2016-06-30
                    • 2012-05-10
                    • 1970-01-01
                    相关资源
                    最近更新 更多