Completion Handler 已执行,但模态视图控制器仍在屏幕上并被引用

Completion Handler is Executed, but Modal View Controller is Still On Screen and Referenced

我的代码正在执行一个冗长的异步操作 (HTTP)。 在开始之前,我展示了一个带有旋转 activity 指示器的自定义模态视图控制器。完成后,我关闭它并显示一个警报控制器以通知用户任务完成。

我的代码如下所示:

// This is a custom modal view controller. It mimics the modal 
// presentation style of UIAlertController, but contains a label 
// and a spinner:
let progress = ActivityIndicatorAlertController(withTitle:"Syncing...")

self.presentViewController(progress, animated:true, completion:{

    WebService.sharedService.syncData(data, completion: {error in

        self.dismissViewControllerAnimated(false, completion: {

            // <BREAKPOINT>

            if error == nil {
                // Update UI:
                self.reloadTable()

                // This method presents a vanilla UIAlertController 
                // with the specified title and an OK 
                // button to dismiss:
                self.presentModalInformation(title:"Sync Complete", message:"")

上面最后一行执行后,我立即在控制台上收到此警告:

Warning: Attempt to present <UIAlertController: 0x7fae3a57ad60> on <AppName.MyPresentingViewController: 0x7fae3b02a800> which is already presenting (null)

尽管正在执行关闭完成处理程序(上面代码中标记为 <BREAKPOINT> 的位置),第一个模态视图控制器(进度指示器)仍然在屏幕上,并且(当然)"completion" 视图永远不会显示控制器。

在用户看来,同步操作 "freezes" 和应用程序变得无响应(挂在模态显示上)。

一些观察:

  1. 为了完整起见,这里是呈现第二个模态视图控制器的方法:

    func presentModalInformation(title title:String, message:String) {
        let alertController = UIAlertController(
            title: title,
            message: message,
            preferredStyle: .Alert)
    
        let okAction = UIAlertAction(title: "OK", 
            style: UIAlertActionStyle.Default, 
            handler: nil)
    
        alertController.addAction(okAction)
    
        self.presentViewController(alertController, animated: true, completion: nil)
    }
    
  2. 在修改通过 HTTP 执行的任务期间,这个问题开始发生在我无法确定的某个时刻。我没有注意到确切的时间,因为我的连接速度很慢并且每次都测试不同的数据集。

  3. UIKit 抱怨说已经有一个模态显示的视图控制器。 确实,如果我在上面代码中标记为<BREAKPOINT>的位置停止执行,并通过控制台检查变量,我得到:

       (lldb) print self.presentedViewController?.classForCoder
       (AnyClass?) $R1 = AppName.ActivityIndicatorAlertController
    

...尽管所有这些都发生在 关闭完成处理程序中

  1. 我试图用 dispatch_after() 一秒来延迟第二个模态视图控制器的呈现(也许让第一个 "time" 关闭正确吗?)但它不起作用。第一个没有被解雇!

  2. 上面代码中的
  3. <BREAKPOINT>是在主线程上执行的("com.apple.main-thread (serial)").

  4. 插入对 dismiss...second 调用,作为完成处理程序中的第一件事, 似乎解决了这个问题:

    self.presentViewController(progress, animated:true, completion:{
    
        WebService.sharedService.syncData(data, completion: {error in
    
            self.dismissViewControllerAnimated(false, completion: {
    
                // <BREAKPOINT>
    
                self.dismissViewControllerAnimated(false, completion:nil)
                // ^^ THIS "FIXES" IT ^^
    

...这是怎么回事?


附录: ActivityIndicatorAlertController 的源代码在 this repository.

看了这里那里,我找到了原因(虽然我还没有完全理解这个问题)。

我决定在更多位置检查 self.presentedViewController?.classForCoder 的值。这是我得到的:

self.presentViewController(progress, animated:true, completion:{

    // (lldb) print self.presentedViewController?.classForCoder
    // (AnyClass?) $R0 = UISearchController

    WebService.sharedService.syncData(data, completion: {error in

        // (lldb) print self.presentedViewController?.classForCoder
        // (AnyClass?) $R0 = UISearchController

        self.dismissViewControllerAnimated(false, completion: { 

            // (lldb) print self.presentedViewController?.classForCoder
            // (AnyClass?) $R1 = AppName.ActivityIndicatorAlertController

所以,在调用dismiss...之前,呈现的controller是一个UISearchController,dismiss后,还剩下ActivityIndicatorAlertController的实例! (难怪再次调用 dismiss.., 会得到预期的结果!)。

讨论

搜索控制器的身份提示了我,我很快意识到这个问题只发生在 一些 数据集:我从 table 单元格按钮,但为了显示 some 行,我必须先点击 [=] 上方搜索栏的 scope 按钮83=] 视图。当我从一开始就可见的行之一同步时(无 table 视图搜索),问题不会发生。

我想这会以某种方式插入“附加的模态视图控制器层”,所以我需要关闭一次以摆脱搜索控制器,然后再关闭一次 my自定义进度控制器。

然而,我不明白如何。我假设每个视图控制器当时可以呈现一个模态视图控制器(毕竟这是 warning/issue 的全部原因!)

搜索控制器似乎占据了模态表示的“不同级别”:激活时,它被报告为 self.presentedViewController 并消耗一次对 dismiss... 的调用。但这 不会 阻止你以模态方式呈现 另一个 视图控制器(在我的例子中,ActivityIndicatorAlertController),它会以某种方式“堆积起来” " 在搜索控制器之上,not 报告为 presentedViewController,并且需要 额外的 调用 dismiss...,然后你可以再展示一个。

这有意义吗?


所以,我通过在整个事情发生之前添加这段代码来修复:

if let self.presentedViewController != nil {
    dismissViewControllerAnimated(false, completion: nil)
    // gets rid of occasional UISearchController
}

(...虽然我猜停用搜索控制器应该更正确)


编辑:

确实,调用:

self.resultSearchController.active = false

...在介绍 activity 指示器警报控制器之前也可以工作(并且更正确)。