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