如何在状态恢复期间等待 CoreData/Network 请求时显示加载微调器?

How to present a loading spinner during State Restoration whilst waiting for CoreData/Network requests?

4 年前我已经在这里询问过状态恢复和 CoreData

State preservation and restoration strategies with Core Data objects in a UIManagedDocument

最后,我的应用程序执行了我描述的操作,并且状态恢复了它想要保存的任何 CoreData 对象的 URIRepresentations。这些对象只能在 CoreData 加载后解析(通过 UIManagedDocument 及其文档加载回调)。一切正常,尽管有时在 CoreData 文档加载期间视图是空的。

对我来说最大的问题是,用户可以尝试在此边缘状态期间与我的应用程序的视图进行交互,并且这样做通常会导致应用程序崩溃,因为新视图设置了空的 CoreData 属性,这些属性需要在他们继续时进行设置。

我需要一个解决方案来解决这个问题,在仍未加载 CoreData 的情况下在每个视图上添加自定义的按钮阻塞等可以管理它,但需要大量重复工作,就用户体验而言,它不是最好的。按下输入时我会发出警报,我们仍在等待 CoreData 加载。

我的首选解决方案是以某种方式覆盖 ViewController 恢复并将新的顶部 viewController 注入到恢复的层次结构中,它可以在加载 CoreData 之前显示微调器。我没有看到任何例子,也没有在文档中看到支持这种策略的适当方法的描述。

最终,如果我可以判断何时 viewController 正在恢复,如果它是顶部 viewController 那么也许我可以推送一个模态加载微调器 viewController。不确定现在是否是推送新 VC 的合适时间,但我想我可以推迟到 ViewWillAppear 或其他一些小型计时器延迟回调。唯一的问题可能是您看到原始视图状态恢复然后更改为微调器..如果我可以让 segue 淡出微调器在这可能不会太刺耳。

有人对此有什么建议吗?这是一些其他应用程序一直在做的事情,例如 Facebook,当它们恢复并进入网络以重新加载您的帖子以供阅读时。

感谢您的宝贵时间

此致

吉姆

你发现自己所处的情况似乎足以成为重新考虑你为实现这一目标所做的事情的理由。我正在使用我猜类似的情况,因为我在单独的线程中加载所有核心数据对象,所以使用像

这样的完成
MyEntity.fetchAll { items,
   self.entities = items
   self.tableView.reloadData()
}

在这种情况下,执行以下操作非常容易:

var entities: [Any]? {
    didSet {
        self.removeActivityIndicator()
    }
}

您可以将所有逻辑放入视图控制器的某个基 class 中,以便您可以轻松地重用它。

虽然有时候静态地做这些事情会更好。您可以在具有 activity 指示符的所有内容上方添加一个新的 window。基本上就像做自定义警报视图。保留计数系统应该是最好的:

class ActivityManager {

    private static var retainCount: Int = 0 {
        didSet {
            if(oldValue > 0 && newValue == 0) removeActivityWindow()
            else if(oldValue == 0 && newValue > 0) showActivityWindow()
        }
    }

    static func beginActivity() { retainCount += 1 }
    static func endActivity() { retainCount -= 1 }
}

在这种情况下,您可以在代码中的任何位置使用该工具。规则是每个 "begin" 必须有一个 "end"。例如:

func resolveData() {
    ActivityManager.beginActivity()
    doMagic {
        ActivityManager.endActivity()
    }
}

确实有很多方法可以做到这一点,可能没有 "best solution",因为这取决于您的情况。

使用新的 window 显示对话框的示例:

根据评论中的要求,我添加了一个关于如何在新 window 中显示对话框的示例。我正在使用一个包含视图控制器 AlertViewController 的新故事板 "Dialog"。这也可能是一个带有一些 activity 指示器的控制器,但更重要的部分是如何生成 window、如何显示控制器以及如何关闭。

class AlertViewController: UIViewController {

    @IBOutlet private var blurView: UIVisualEffectView?
    @IBOutlet private var dialogPanel: UIView?
    @IBOutlet private var titleLabel: UILabel? // Is in vertical stack view
    @IBOutlet private var messageLabel: UILabel? // Is in vertical stack view
    @IBOutlet private var okButton: UIButton? // Is in horizontal stack view
    @IBOutlet private var cancelButton: UIButton? // Is in horizontal stack view

    var titleText: String?
    var messageText: String?
    var confirmButtonText: String?
    var cancelButtonText: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        setHiddenState(isHidden: true, animated: false) // Initialize as not visible

        titleLabel?.text = titleText
        titleLabel?.isHidden = !(titleText?.isEmpty == false)

        messageLabel?.text = messageText
        messageLabel?.isHidden = !(messageText?.isEmpty == false)

        okButton?.setTitle(confirmButtonText, for: .normal)
        okButton?.isHidden = !(confirmButtonText?.isEmpty == false)

        cancelButton?.setTitle(cancelButtonText, for: .normal)
        cancelButton?.isHidden = !(cancelButtonText?.isEmpty == false)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setHiddenState(isHidden: false, animated: true)
    }

    private func setHiddenState(isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil) {
        UIView.animate(withDuration: animated ? 0.3 : 0.0, animations: {
            self.blurView?.effect = isHidden ? UIVisualEffect() : UIBlurEffect(style: .light)
            self.dialogPanel?.alpha = isHidden ? 0.0 : 1.0
        }) { _ in
            completion?()
        }
    }

    @IBAction private func okPressed() {
        AlertViewController.dismissAlert()
    }
    @IBAction private func cancelPressed() {
        AlertViewController.dismissAlert()
    }


}

// MARK: - Window

extension AlertViewController {

    private static var currentAlert: (window: UIWindow, controller: AlertViewController)?

    static func showMessage(_ message: String) {

        guard currentAlert == nil else {
            print("An alert view is already shown. Dismiss this one to show another.")
            return
        }

        let controller = UIStoryboard(name: "Dialog", bundle: nil).instantiateViewController(withIdentifier: "AlertViewController") as! AlertViewController
        controller.confirmButtonText = "OK"
        controller.messageText = message

        let window = UIWindow(frame: UIApplication.shared.windows[0].frame)
        window.windowLevel = .alert
        window.rootViewController = controller
        window.makeKeyAndVisible()

        self.currentAlert = (window, controller)
    }

    static func dismissAlert() {
        if let currentAlert = self.currentAlert {
            currentAlert.controller.setHiddenState(isHidden: true, animated: true) {
                self.currentAlert?.window.isHidden = true
                self.currentAlert = nil
            }
        }
    }

}

我添加了整个 class 以防万一,但重要的部分是显示新的 window:

let window = UIWindow(frame: UIApplication.shared.windows[0].frame) // Create a window
window.windowLevel = .alert // Define which level it should be in
window.rootViewController = controller // Give it a root view controller
window.makeKeyAndVisible() // Show the window

并删除 window:

window.isHidden = true

只需隐藏您的 window 就足够了。假设您对它没有任何强引用,它将从应用程序堆栈中删除。要确认这一点,请确保 UIApplication.shared.windows.count 具有适当的值,在大多数情况下,当显示警报时应为 2,否则为 1

我对上面代码的测试用法很简单:

AlertViewController.showMessage("A test message. This is testing of alert view in a separate window.")