1

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.

siburb
  • 4,880
  • 1
  • 25
  • 34
Hobbyist
  • 15,888
  • 9
  • 46
  • 98

2 Answers2

6

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for the information about the maximum refresh rate of 60fps, I didn't know that the devices were capped. It's also convenient that this is already linked to the main thread. Accepted. – Hobbyist Jan 22 '16 at 12:10
  • I had gone back and finally implemented this, however the progress for the bar is still not updating. - The timer is executing logic correctly, but the UI is not updating like it should. – Hobbyist Feb 19 '16 at 22:54
  • I just tested this again and it works fine. Thus, if it's not working for you, you should check a few things: 1. Are you hitting the code starting the run loop? Are you hitting the code inside `handleDisplayLink`? Add `print` statements in both and make sure it's hitting the code you think it is. 2. Make sure you're scheduling the display link on the main runloop. 3. Make sure you're not doing anything else to block the main thread. Without seeing your full code, it's hard to diagnose, but it's going to be something simple like that. – Rob Feb 19 '16 at 23:57
  • I had implemented it exactly how you had showed, and can guarantee that there's nothing blocking the main thread. The timer is working as expected, and my debug printing is working fine, as-well as the `percentageComplete` variable updating like it should. It's just the progress does not visually update on the progress bar. – Hobbyist Feb 20 '16 at 19:20
  • Then I think you should post another question with code snippet and describe what diagnostics you've done. Or upload your project somewhere and I can take a look at it. – Rob Feb 20 '16 at 22:38
3

You must always update the UI on the main thread. I'm not a Swift expert, but it looks like you're currently trying to update the UIProgressView in the background.

Try updating the UIProgressView on the main queue like this:

dispatch_async(dispatch_get_main_queue(),^{
    self.timerBar.setProgress(Float(self.timeRemaining) / 15.0, animated: true)
    print(String(self.timerBar.progress))
})
Brian
  • 14,610
  • 7
  • 35
  • 43
siburb
  • 4,880
  • 1
  • 25
  • 34