肇鑫的技术博客

业精于勤,荒于嬉

NSProgressIndicator in spinstyle

NSProgressIndicator采用spinstyle时,如果还有后续的操作,系统会优先执行后续操作,在操作结束后,才运行状态条。这是我们所不希望的。此时需要暂时停止RunLoop.main,待状态条开始运行后,再进行后续的工作。

func userDefaultsDidChange() {
    guard let controller = tableViewController else { return }
    
    let progressIndicator = { () -> NSProgressIndicator in
        let frame = view.frame
        let x = (frame.width - 50) / 2
        let y = (frame.height - 50) / 2
        let piFrame = NSMakeRect(x, y, 50, 50)
        let pi = NSProgressIndicator(frame: piFrame)
        pi.style = .spinningStyle
        
        return pi
    }()
    
    view.addSubview(progressIndicator)
    progressIndicator.startAnimation(self)
    
    RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.001))
    
    controller.run()
    
    progressIndicator.stopAnimation(self)
    progressIndicator.removeFromSuperview()
}

参考资料

注册`UserDefaults.didChangeNotification`的技巧

UserDefaults.didChangeNotification应该在PreferencesViewController里进行注册,而不是在需要变化的ViewController里。因为如果是在后者注册,程序运行时,可能会出现意想不到的异常。即用户在没有打开设置的情况下,设置变了,而造成你的程序的异常。

今天下午排查了很久,最后发现是这个原因。

ProgressBar与进度

// MARK: - progress bar with file dealing process
private var counter:Int = 0

func process() {
    // get total

    // prepare alert
    let screenFrame = NSScreen.main()!.frame
    let window = NSWindow(contentRect: NSMakeRect(screenFrame.width / 2 - 140, screenFrame.height * 0.66 + 50, 280, 20),
                          styleMask: NSBorderlessWindowMask,
                          backing: .buffered, defer: false)
    window.isOpaque = false

    let alert = NSAlert()

    alert.addButton(withTitle: "OK")
    alert.buttons.first!.isHidden = true

    // prepare prgreessBar
    let progressBar = NSProgressIndicator(frame: NSMakeRect(0,0,296,20))
    progressBar.isIndeterminate = false
    progressBar.maxValue = 1000
    progressBar.doubleValue = 0
    let step = progressBar.maxValue / Double(total)
    alert.accessoryView = progressBar
    alert.messageText = "0/\(total)"
    alert.beginSheetModal(for: window) { [unowned self] (buttonId) in
        alert.window.orderOut(self)
    }

    let userInfo:[String:Any] = ["progress bar": progressBar, "step": step, "alert": alert, "total": total]

    // timer to update progress bar UI
    self.counter = 0
    let timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(updateProgressbar(timer:)), userInfo: userInfo, repeats: true)

    DispatchQueue.global().async() { [unowned self] () -> () in
        func excute() {
            // MARK: - do works
            // for in loop 
            for _ in 0 ... total {
                // update counter
                self.counter += 1
            }
        }

        excute()

        DispatchQueue.main.async { [unowned self] () -> () in
            timer.invalidate()
            progressBar.doubleValue = 1000
            alert.messageText = "\(total)/\(total)"

            alert.buttons.first!.performClick(self)
        }
    }
}

func updateProgressbar(timer:Timer) {
    DispatchQueue.main.async { [unowned self] () -> () in
        if let dic = timer.userInfo as? [String:Any], let progressBar = dic["progress bar"] as? NSProgressIndicator, let step = dic["step"] as? Double, let alert = dic["alert"] as? NSAlert, let total = dic["total"] as? Int {
            progressBar.doubleValue = step * Double(self.counter)
            alert.messageText = "\(self.counter)/\(total)"
        }
    }
}

有趣的是,hidden的按钮在程序里是可以点击的。

另外,必须通过模拟点击的方式才能正确退出alert。采用其它方式有可能造成UI异常,甚至NSWindow不能正确释放而导致程序退出时崩溃。