Swift:确定NSProgressIndicator,异步刷新并等待返回

时间:2017-03-28 22:33:04

标签: swift cocoa asynchronous grand-central-dispatch nsprogressindicator

在Swift3中工作;我在循环中运行了一个相当昂贵的操作,迭代遍历并将其构建为一个数组,返回时将用作NSTableView的内容。

我想要一张显示进度的模态表,这样人们就不会认为应用程序被冻结了。通过谷歌搜索,在这里四处寻找,而不是少量的试验和错误,我已设法实现我的进度条,并随着循环的进展充分展示进展。

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

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

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

1 个答案:

答案 0 :(得分:2)

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

您明确地在allUTIs的初始化中成功诊断出性能问题,并且在异步执行此操作时是谨慎的,您不应该在allUTIs属性的初始化块中执行此操作。将启动allUTIs更新的此代码移动到单独的函数中。

查看ProgressBar,它确实是一个警报,因此我将其称为ProgressAlert以明确这一点,但会公开必要的方法来更新该警报中的NSProgressIndicator:< / p>

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")
}

下面,你说:

  

实际上,这意味着allUTIs仅在第一次选择适当的TabViewItem时实际启动(这就是为什么我用这样的闭包初始化它)。所以,我不确定如何重构这个,或者我应该在哪里移动实际的初始化。请记住,我几乎是一个新手;这是我的第一个Swift(也是Cocoa)项目,我已经学习了几个星期。

如果要在选择选项卡时将其实例化,则挂钩到子视图控制器viewDidLoad。或者您可以在标签视图控制器的tab​View(_:​did​Select:​)

中执行此操作

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

所以,如果我考虑进行更激进的重新设计,我可能会首先更改我的模型对象以进一步将其更新过程与任何特定UI隔离,而只是简单地返回(和更新)Progress对象。 / p>

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。警报会阻止整个窗口,您可能希望仅阻止与该一个选项卡的交互。