Swift:判断NSProgressIndicator,异步刷新等待return

Swift: determinate NSProgressIndicator, async refreshing and waiting for return

正在使用 Swift3;我在一个循环中有一个非常昂贵的操作 运行 遍历内容并将其构建到一个数组中,该数组在 return 上将用作 NSTableView 的内容。

我想要一个模式 sheet 来显示这方面的进展,这样人们就不会认为应用程序被冻结了。通过谷歌搜索,在这里环顾四周,而不是少量的试验和错误,我已经设法实现了我的进度条,并让它在循环进行时充分显示进度。

现在的问题是什么?尽管 sheet(作为 NSAlert 实现,进度条在附件视图中)完全按预期工作,但在循环完成之前 returns 的整个过程。

这是代码,希望有人能告诉我我做错了什么:

class ProgressBar: NSAlert {
    var progressBar = NSProgressIndicator()
    var totalItems: Double = 0
    var countItems: Double = 0
    override init() {
        progressBar.isIndeterminate = false
        progressBar.style = .barStyle
        super.init()
        self.messageText = ""
        self.informativeText = "Loading..."
        self.accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
        self.accessoryView?.addSubview(progressBar)
        self.layout()
        self.accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
        self.addButton(withTitle: "")
        progressBar.sizeToFit()
        progressBar.setFrameSize(NSSize(width:290, height: 16))
        progressBar.usesThreadedAnimation = true
        self.beginSheetModal(for: ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window!, completionHandler: nil)
    }
}

static var allUTIs: [SWDAContentItem] = {
    var wrappedUtis: [SWDAContentItem] = []
    let utis = LSWrappers.UTType.copyAllUTIs()

        let a = ProgressBar()

        a.totalItems = Double(utis.keys.count)
        a.progressBar.maxValue = a.totalItems

        DispatchQueue.global(qos: .default).async {
            for uti in Array(utis.keys) {
                a.countItems += 1.0
                wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
                Thread.sleep(forTimeInterval:0.0001)

                DispatchQueue.main.async {
                    a.progressBar.doubleValue = a.countItems
                    if (a.countItems >= a.totalItems && a.totalItems != 0) {
                        ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window?.endSheet(a.window)
                    }
                }
            }
        }
    Swift.print("We'll return now...")
    return wrappedUtis // This returns before the loop is finished.
}()

简而言之,您在异步代码有机会完成之前 returning wrappedUtis。如果更新过程本身是异步发生的,则不能使初始化闭包 return 成为一个值。

您显然在 allUTIs 的初始化中成功诊断出性能问题,虽然异步执行此操作是明智的,但您不应该在 allUTIs 的初始化块中执行此操作 属性。将启动 allUTIs 更新的代码移动到单独的函数中。


查看 ProgressBar,它确实是一个警报,所以我将其称为 ProgressAlert 以明确这一点,但公开了更新该警报中的 NSProgressIndicator 的必要方法:

class ProgressAlert: NSAlert {
    private let progressBar = NSProgressIndicator()

    override init() {
        super.init()

        messageText = ""
        informativeText = "Loading..."
        accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
        accessoryView?.addSubview(progressBar)
        self.layout()
        accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
        addButton(withTitle: "")

        progressBar.isIndeterminate = false
        progressBar.style = .barStyle
        progressBar.sizeToFit()
        progressBar.setFrameSize(NSSize(width:290, height: 16))
        progressBar.usesThreadedAnimation = true
    }

    /// Increment progress bar in this alert.

    func increment(by value: Double) {
        progressBar.increment(by: value)
    }

    /// Set/get `maxValue` for the progress bar in this alert

    var maxValue: Double {
        get {
            return progressBar.maxValue
        }
        set {
            progressBar.maxValue = newValue
        }
    }
}

注意,这不显示 UI。这是提出它的人的工作。

然后,与其在初始化闭包中启动这个异步填充(因为初始化应该始终是同步的),不如创建一个单独的例程来填充它:

var allUTIs: [SWDAContentItem]?

private func populateAllUTIs(in window: NSWindow, completionHandler: @escaping () -> Void) {
    let progressAlert = ProgressAlert()
    progressAlert.beginSheetModal(for: window, completionHandler: nil)

    var wrappedUtis = [SWDAContentItem]()
    let utis = LSWrappers.UTType.copyAllUTIs()

    progressAlert.maxValue = Double(utis.keys.count)

    DispatchQueue.global(qos: .default).async {
        for uti in Array(utis.keys) {
            wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))

            DispatchQueue.main.async { [weak progressAlert] in
                progressAlert?.increment(by: 1)
            }
        }
        DispatchQueue.main.async { [weak self, weak window] in
            self?.allUTIs = wrappedUtis
            window?.endSheet(progressAlert.window)
            completionHandler()
        }
    }
}

现在,您将 allUTIs 声明为 static,因此您也可以调整上面的内容来做到这一点,但将其设为实例变量似乎更合适。

无论如何,您可以用类似以下内容填充该数组:

populateAllUTIs(in: view.window!) {
    // do something
    print("done")
}

下面,你说:

In practice, this means allUTIs is only actually initiated when the appropriate TabViewItem is selected for the first time (which is why I initialize it with a closure like that). So, I'm not really sure how to refactor this, or where should I move the actual initialization. Please keep in mind that I'm pretty much a newbie; this is my first Swift (also Cocoa) project, and I've been learning both for a couple of weeks.

如果你想在选择选项卡时实例化它,然后挂接到子视图控制器 viewDidLoad. Or you can do it in the tab view controller's tab​View(_:​did​Select:​)

但是如果allUTIs的人口这么慢,你确定要偷懒吗?为什么不尽快触发这个实例化,这样当用户选择该选项卡时就不太可能出现延迟。在这种情况下,您可能会触发它自己的选项卡视图控制器 viewDidLoad,这样需要这些 UTI 的选项卡更有可能拥有它们。

因此,如果我正在考虑进行更激进的重新设计,我可能会首先更改我的模型对象以进一步将其更新过程与任何特定 UI 隔离开来,而不是简单地 return(并更新) 一个 Progress 对象。

class Model {

    var allUTIs: [SWDAContentItem]?

    func startUTIRetrieval(completionHandler: (() -> Void)? = nil) -> Progress {
        var wrappedUtis = [SWDAContentItem]()
        let utis = LSWrappers.UTType.copyAllUTIs()

        let progress = Progress(totalUnitCount: Int64(utis.keys.count))

        DispatchQueue.global(qos: .default).async {
            for uti in Array(utis.keys) {
                wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))

                DispatchQueue.main.async {
                    progress.completedUnitCount += 1
                }
            }

            DispatchQueue.main.async { [weak self] in
                self?.allUTIs = wrappedUtis
                completionHandler?()
            }
        }

        return progress
    }

}

然后,我可以让选项卡栏控制器实例化它并与任何需要它的视图控制器共享进度:

class TabViewController: NSTabViewController {

    var model: Model!

    var progress: Progress?

    override func viewDidLoad() {
        super.viewDidLoad()

        model = Model()
        progress = model.startUTIRetrieval()

        tabView.delegate = self
    }

    override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
        super.tabView(tabView, didSelect: tabViewItem)

        if let item = tabViewItem, let controller = childViewControllers[tabView.indexOfTabViewItem(item)] as? ViewController {
            controller.progress = progress
        }
    }

}

然后视图控制器可以观察这个 Progress 对象,以确定它是否需要更新它的 UI 来反映这个:

class ViewController: NSViewController {

    weak var progress: Progress? { didSet { startObserving() } }

    weak var progressAlert: ProgressAlert?

    private var observerContext = 0

    private func startObserving() {
        guard let progress = progress, progress.completedUnitCount < progress.totalUnitCount else { return }

        let alert = ProgressAlert()
        alert.beginSheetModal(for: view.window!)
        progressAlert = alert
        progress.addObserver(self, forKeyPath: "fractionCompleted", context: &observerContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let progress = object as? Progress, context == &observerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        dispatchPrecondition(condition: .onQueue(.main))

        if progress.completedUnitCount < progress.totalUnitCount {
            progressAlert?.doubleValue = progress.fractionCompleted * 100
        } else {
            progress.removeObserver(self, forKeyPath: "fractionCompleted")
            view.window?.endSheet(progressAlert!.window)
        }
    }

    deinit {
        progress?.removeObserver(self, forKeyPath: "fractionCompleted")
    }

}

而且,在这种情况下,ProgressAlert 只会担心 doubleValue:

class ProgressAlert: NSAlert {
    private let progressBar = NSProgressIndicator()

    override init() {
        super.init()

        messageText = ""
        informativeText = "Loading..."
        accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
        accessoryView?.addSubview(progressBar)
        self.layout()
        accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
        addButton(withTitle: "")

        progressBar.isIndeterminate = false
        progressBar.style = .barStyle
        progressBar.sizeToFit()
        progressBar.setFrameSize(NSSize(width: 290, height: 16))
        progressBar.usesThreadedAnimation = true
    }

    /// Set/get `maxValue` for the progress bar in this alert

    var doubleValue: Double {
        get {
            return progressBar.doubleValue
        }
        set {
            progressBar.doubleValue = newValue
        }
    }
}

不过,我必须注意,如果这些 UTI 只需要那个选项卡,那么就会提出一个问题,即您是否应该使用基于 NSAlert 的 UI。警报会阻止整个 window,您可能只想阻止与那个选项卡的交互。