使用 Swift3;我有一个非常昂贵的操作在一个循环中运行,循环遍历内容并将其构建到一个数组中,该数组在返回时将用作 NSTableView 的内容。
我想要一个模式表来显示这方面的进展,这样人们就不会认为应用程序被卡住了。通过谷歌搜索、环顾四周以及大量的反复试验,我设法实现了我的进度条,并让它在循环进行时充分显示进度。
现在的问题是什么?尽管工作表(作为 NSAlert 实现,进度条在附件 View 中)完全按预期工作,但整个过程在循环完成之前返回。
这是代码,希望有人能告诉我我做错了什么:
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
的初始化 block 中执行此操作属性(property)。移动启动 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 appropriateTabViewItem
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.
如果你想在选择选项卡时实例化它,然后 Hook 到 subview Controller viewDidLoad
.或者您可以在选项卡 View Controller 的 tabView(_:didSelect:)
中执行此操作
但是如果 allUTIs
的人口这么慢,你确定要偷懒吗?为什么不尽快触发这个实例化,这样当用户选择该选项卡时就不太可能出现延迟。在这种情况下,您可能会触发选项卡 View Controller 自己的 viewDidLoad
, 因此需要这些 UTI 的选项卡更有可能拥有它们。
因此,如果我正在考虑进行更彻底的重新设计,我可能会首先更改我的模型对象以进一步将其更新过程与任何特定的 UI 隔离开来,而不是简单地返回(并更新)一个 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
}
}
然后,我可能会让标签栏 Controller 实例化它并与任何需要它的 View Controller 共享进度:
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
}
}
}
然后 View Controller 可以观察到这个Progress
对象,以确定它是否需要更新其 UI 以反射(reflect)这一点:
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。警报会阻止整个窗口,您可能只想阻止与那个选项卡的交互。
关于Swift:判断NSProgressIndicator,异步刷新等待返回,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43081122/