Search code examples
iosmultithreadingswiftuiprogressview

UIProgressView won't update progress when updated from a dispatch


I'm trying to make a progress bar act as a timer and count down from 15 seconds, here's my code:

private var timer: dispatch_source_t!
private var timeRemaining: Double = 15

override public func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    profilePicture.layer.cornerRadius = profilePicture.bounds.width / 2

    let queue = dispatch_queue_create("buzz.qualify.client.timer", nil)
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC, 5 * NSEC_PER_MSEC)
    dispatch_source_set_event_handler(timer) {
        self.timeRemaining -= 0.01;
        self.timerBar.setProgress(Float(self.timeRemaining) / 15.0, animated: true)
        print(String(self.timerBar.progress))
    }
    dispatch_resume(timer)
}

The print() prints the proper result, but the progress bar never updates, somestimes it will do a single update at around 12-15% full and just JUMP there and then do nothing else.

How can I make this bar steadily flow down, and then execute a task at the end of the timer without blocking the UI thread.


Solution

  • In siburb's answer, he correctly points out that should make sure that UI updates happen on the main thread.

    But I have a secondary observation, namely that you're doing 100 updates per second, and there's no point in doing it that fast because the maximum screen refresh rate is 60 frames per second.

    However, a display link is like a timer, except that it's linked to the screen refresh rate. You could do something like:

    var displayLink: CADisplayLink?
    var startTime: CFAbsoluteTime?
    let duration = 15.0
    
    func startDisplayLink() {
        startTime = CFAbsoluteTimeGetCurrent()
        displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
        displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
    }
    
    func stopDisplayLink() {
        displayLink?.invalidate()
        displayLink = nil
    }
    
    func handleDisplayLink(displayLink: CADisplayLink) {
        let percentComplete = Float((CFAbsoluteTimeGetCurrent() - startTime!) / duration)
        if percentComplete < 1.0 {
            self.timerBar.setProgress(1.0 - percentComplete, animated: false)
        } else {
            stopDisplayLink()
            self.timerBar.setProgress(0.0, animated: false)
        }
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
    
        startDisplayLink()
    }
    

    Alternatively, if you're doing something on a background thread that wants to post updates to UIProgressView faster than the main thread can service them, then I'd post that to the main thread using a dispatch source of type DISPATCH_SOURCE_TYPE_DATA_ADD.

    But, if you're just trying to update the progress view over some fixed period of time, a display link might be better than a timer.