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

Swift:确定NSPROGROSENDICATOR,异步刷新并等待返回,swift,cocoa,asynchronous,grand-central-dispatch,nsprogressindicator,Swift,Cocoa,Asynchronous,Grand Central Dispatch,Nsprogressindicator,在迅捷3工作;我在一个循环中运行了一个非常昂贵的操作,循环遍历内容并将其构建到一个数组中,该数组在返回时将用作NSTableView的内容 我想要一个模式表显示这方面的进展,这样人们就不会认为这个应用程序是冻结的。通过谷歌搜索,在这里环顾四周,进行了大量的尝试和错误,我成功地实现了我的progressbar,并让它在循环过程中充分显示进度 现在的问题是什么?即使工作表(实现为NSAlert,进度条在accesory视图中)完全按照预期工作,但整个过程在循环完成之前返回 这是代码,希望有人能告诉我

在迅捷3工作;我在一个循环中运行了一个非常昂贵的操作,循环遍历内容并将其构建到一个数组中,该数组在返回时将用作NSTableView的内容

我想要一个模式表显示这方面的进展,这样人们就不会认为这个应用程序是冻结的。通过谷歌搜索,在这里环顾四周,进行了大量的尝试和错误,我成功地实现了我的progressbar,并让它在循环过程中充分显示进度

现在的问题是什么?即使工作表(实现为NSAlert,进度条在accesory视图中)完全按照预期工作,但整个过程在循环完成之前返回

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

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

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

显然,您在初始化
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")
}

下面,你说:


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

如果要在选中选项卡时实例化此项,请挂接到子视图控制器。也可以在选项卡视图控制器的

但是,如果
allUTIs
的人口增长如此缓慢,您确定要懒洋洋地这样做吗?为什么不早点触发这个实例化,这样当用户选择那个选项卡时就不太可能有延迟了。在这种情况下,您可以通过选项卡视图控制器自己的
viewdiload
触发它,以便需要这些UTI的选项卡更有可能拥有这些UTI

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

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

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

}
然后,视图控制器可以观察此
进度
对象,以确定是否需要更新其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")
    }

}

但是,我必须注意,如果这些UTI仅用于这一个选项卡,那么就会产生一个问题,即您是否应该使用基于
NSAlert
的UI。警报会阻止整个窗口,您可能只想阻止与该选项卡的交互。

一些与外围设备相关的观察结果:1。不要从后台线程更新
progressBar.doubleValue
。它实际上触发了一个UI更新,所有这些都是它自己触发的,并且您永远不希望从后台线程更新UI。确保在主线程上分派的代码中发生了
doubleValue
的更新。(顺便说一句,
setNeedsDisplay
displayIfNeeded
不是必需的。)。通常,您希望职责分离。例如,实例化
ProgressBar
模型的代码不应更新UI。为allUTIs设置初始值的代码不应该启动异步进程,更不用说更新UI了。你不应该在主线程上睡觉。一旦我完成了,我肯定会把实际的进度条码移到别处
allUTIs
实际上是我的模型对象的属性之一。为了更全面地了解我在这里所做的工作:我有一个动态生成的选项卡视图,其中有一个
NSTableView
,它的内容绑定到
[AnyObject]
。这是一个计算属性,用于检查当前活动的TabViewItem是什么,并在模型对象中查询适当的数组;在这种情况下,
allUtis
。实际上,这意味着只有在第一次选择适当的TabViewItem时才会实际启动
allUtis
(这就是为什么我用这样的闭包初始化它)。因此,我不确定如何重构它,或者我应该将实际的初始化移到哪里。请记住,我几乎是一个新手;这是我的第一个Swift(也是Cocoa)项目,我已经学习了两个星期。请参阅上面的两组代码片段,其中一组我考虑对代码进行更温和的更改,另一组我进行了更彻底的重构。非常感谢;我决定采用你的第一种方法,只做一些改动。简而言之,我使变量和函数保持静态(因为它是模型类,所以
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")
    }

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