UIProgressView won't update progress when updated from a dispatch - ios

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.

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.

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))
})

Related

Swift 2 - Print New Value from Slider at Regular Interval

I am trying to print a value from a slider at regular intervals. But only print the value if it is different to that last printed. I also do not want to miss any of the output values from the slider.
To do this I have created an array and added an element to the start of that array if it is different to the one already at the start. I have then used a repeating NSTimer to regularly call a function that prints the last element in the array before removing it from the array.
What happens when I run the app is the NSTimer stops anything being printed for it's set time, but then all of the elements print at once and more than one of each print. I've tried messing about with lots of different things - this is the closest I have got to making it work.
If you need to know any more info let me know.
I really appreciate any help given, thanks very much.
var sliderArray: [Float] = []
var timer: NSTimer!
let step: Float = 1
#IBAction func sliderValueChanged(sender: AnyObject)
{
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
func sendSliderPosition()
{
if sliderArray.count > 0
{
print(self.sliderArray.last)
sliderArray.removeLast()
}
}
I would suggest using CADisplayLink. A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. Which is perfect for your slider case.
This will also not trigger unnecessary call when the slider or the UI is at rest.
class C: UIViewController {
var displayLinkTimer: CADisplayLink?
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
displayLinkTimer = CADisplayLink(target: self, selector: #selector(printSliderValue))
let runLoop = NSRunLoop.mainRunLoop()
displayLinkTimer?.addToRunLoop(runLoop, forMode: runLoop.currentMode ?? NSDefaultRunLoopMode )
displayLinkTimer?.paused = true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
displayLinkTimer?.paused = true
}
deinit {
displayLinkTimer?.invalidate()
}
func printSliderValue()
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
print(roundedValue)
}
}
The basic idea is this:
--> Every time the screen needs to redraw (this will happen at max around 60 frames per second taking into consideration this is fps rate), we get a chance to perform function.
--> to do so, we add the displayLink to the Run Loop. (Run lopp processes input/ refreshes UI and time slices)
--> NOTE This method wont be called if there is no redraw needed on the screen. This is not a timer per say that fires periodically. It fires when redraw is needed. In Sliders case, we want this to fire when we move slightest of the slider too.
For more info on how it actually works try it out and see the apple documentation. Make sure to invalidate before deinitializing the ViewController.
Figured out the answer, thank to everyone for the help and suggestions:
override func viewDidLoad()
{
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.02, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
#IBAction func sliderValueChanged(sender: AnyObject)
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
}
func sendSliderPosition()
{
if sliderArray.count > 1
{
let end1 = sliderArray.count-2
print(sliderArray[end1])
sliderArray.removeLast()
}
}
Explanation:
If the new slider value is different to the one already in the array then add it to the array at the start. Use an NSTimer to repeatedly call the sendSliderPosition function from viewDidLoad. The function will only be performed if there is more than one element in the array. If there is, print the element before the last one and remove the last. This always ensures that there is one element in the array so the function does not always run and that the element printed is the most recent one that hasn't already been printed.

UIProgressView not working as expected with NSTimer - Swift

I am using an NSTimer to let the user know the app is working. The progress bar is set up to last 3 seconds, but when running, it displays in a 'ticking' motion and it is not smooth like it should be. Is there anyway I can make it more smooth - I'm sure just a calculation error on my part.
Here is the code:
import UIKit
class LoadingScreen: UIViewController {
var time : Float = 0.0
var timer: NSTimer?
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
// Do stuff
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector:Selector("setProgress"), userInfo: nil, repeats: true)
} //close viewDidLoad
func setProgress() {
time += 0.1
progressView.progress = time / 3
if time >= 3 {
timer!.invalidate()
}
}
}
As per Apple iOS SDK docs you can achieve it with the use of next API:
func setProgress(_ progress: Float, animated animated: Bool)
It adjusts the current progress shown by the receiver, optionally animating the change.
Parameters:
progress - The new progress value.
animated - true if the change should be animated, false if the change should happen immediately.
More info on this:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIProgressView_Class/index.html#//apple_ref/occ/instm/UIProgressView/setProgress:animated:
So in your case you should do it like this:
func setProgress() {
time += 0.1
dispatch_async(dispatch_get_main_queue()) {
progressView.setProgress(time / 3, animated: true)
}
if time >= 3 {
timer!.invalidate()
}
}
Also please note that it is a good practice to perform UI updates on main thread, so I just dispatched progress update on main queue.
Hope it will help you.

Better alternative to timer based animation for custom activity indicator?

I have a class that displays a custom indeterminate progress indicator. Each timer update it simply increments the rotation of a UIImageView using CGAffineTransformRotate.
This all works, however, I noticed that when it is running, the background process that it is waiting for runs 50% slower - which is a huge penalty. For instance; instead of taking say 20 seconds to complete the processing it takes 30 seconds. Can someone recommend a solution with less performance penalty?
func show() {
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(0.03, target: self, selector: #selector(self.updateTimer(_:)), userInfo: nil, repeats: true)
}
func updateTimer(sender: NSTimer) {
iconView.transform = CGAffineTransformRotate(iconView.transform, 0.15)
}
Use Core Animation to animate the rotation. The window server will do all the work outside of your app's process.
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 2 * M_PI
animation.repeatCount = .infinity
animation.duration = 1.25
iconView.layer.addAnimation(animation, forKey: animation.keyPath)
I think you can use dispatch_source from #Rob's answer:
Do something every x minutes in Swift
Here is the code:
var timer: dispatch_source_t!
func startTimer() {
let queue = dispatch_queue_create("com.domain.app.timer", nil)
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 60 * NSEC_PER_SEC, 1 * NSEC_PER_SEC) // every 60 seconds, with leeway of 1 second
dispatch_source_set_event_handler(timer) {
// do whatever you want here
}
dispatch_resume(timer)
}
func stopTimer() {
dispatch_source_cancel(timer)
timer = nil
}

Progress for LongPressureGesture on a Button

I got a button with a LongPressureGesture, and i would like to have a small ProgressView on top of this button as visual feedback for the user that the longPressureGesture is recognized.
I'm stuck on how to detect the beginning of the longPressure and the duration of the longPressure to be able to set the setProgress() on my ProgressView.
EDIT: So i inspired myself from the answers, thank you. Here is what i made. Feel free to comment the following code, maybe there is a better solution.
private var lpProgress:Float = 0
private var startTouch: NSTimer!
#IBAction func pauseJogButtonTouchDown(sender: AnyObject) {
self.startTouch = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "updateLPGestureProgressView", userInfo: nil, repeats: true)
}
func updateLPGestureProgressView() {
self.lpProgress += 0.1
self.lpGestureProgressView.setProgress(self.lpProgress, animated: true)
if self.lpProgress >= 1 {
self.startTouch.invalidate()
self.pauseBarButton.hidden = true
self.lpGestureProgressView.setProgress(0.0, animated: false)
self.toolbarHomeMadeView.hidden = false
self.switchToState(.Paused)
}
}
#IBAction func pauseJogButtonTouchUpInside(sender: AnyObject) {
self.lpProgress = 0
self.startTouch.invalidate()
self.lpGestureProgressView.setProgress(0.0, animated: false)
}
You do not need the LongPressureGesture in this case.
Use "Touch Down" IBAction of UIButton to start NSTimer, and "Touch Up Inside" to stop timer and check if the delay was right.
ProgressView you can fill by timer progress.
Set up an NSTimer on touchesBegan.
At the same time start your animation to animate the view.
When touchesEnded is triggered then stop the animation if the NSTimer has not triggered yet and cancel the timer.
When the timer finishes run your desired action.
Long Press isn't really designed for this sort of thing.

Swift - UIProgressView is not smooth with NSTimer

So I am using an NSTimer to let the user know the app is working. The progress bar is set up to last 3 seconds, but when running, it displays in a 'ticking' motion and it is not smooth like it should be. Is there anyway I can make it more smooth - I'm sure just a calculation error on my part....
If anyone could take a look that would be great. Here is the code:
import UIKit
class LoadingScreen: UIViewController {
var time : Float = 0.0
var timer: NSTimer?
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
// Do stuff
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector:Selector("setProgress"), userInfo: nil, repeats: true)
}//close viewDidLoad
func setProgress() {
time += 0.1
progressView.progress = time / 3
if time >= 3 {
timer!.invalidate()
}
}
}
Edit: A simple 3 second UIView animation (Recommended)
If your bar is just moving smoothly to indicate activity, possibly consider using a UIActivityIndicatorView or a custom UIView animation:
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
UIView.animateWithDuration(3, animations: { () -> Void in
self.progressView.setProgress(1.0, animated: true)
})
}
Make sure your progressView's progress is set to zero to begin with. This will result in a smooth 3 second animation of the progress.
Simple animated progress (Works but still jumps a bit)
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIProgressView_Class/#//apple_ref/occ/instm/UIProgressView/setProgress:animated:
func setProgress() {
time += 0.1
progressView.setProgress(time / 3, animated: true)
if time >= 3 {
timer!.invalidate()
}
}
Option with smaller intervals. (Not recommended)
Set your timer to a smaller interval:
timer = NSTimer.scheduledTimerWithTimeInterval(0.001, target: self, selector:Selector("setProgress"), userInfo: nil, repeats: true)
Then update your function
func setProgress() {
time += 0.001
progressView.setProgress(time / 3, animated: true)
if time >= 3 {
timer!.invalidate()
}
}
For continues loader
timer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(setProgress), userInfo: nil, repeats: true)
and
func setProgress() {
time += 0.001
downloadProgressBar.setProgress(time / 3, animated: true)
if time >= 3 {
self.time = 0.001
downloadProgressBar.progress = 0
let color = self.downloadProgressBar.progressTintColor
self.downloadProgressBar.progressTintColor = self.downloadProgressBar.trackTintColor
self.downloadProgressBar.trackTintColor = color
}
It's hard to say exactly what the problem is. I would like to see the output if you put a print line in setProgress to print a timestamp. Is it actually firing every tenth of a second? My guess is that it is not.
Why not? Well, the timer schedules a run loop task in the main thread to execute the code in setProgress. This task cannot run until tasks in front of it in the queue do. So if there are long running tasks happening in your main thread, your timer will fire very imprecisely. My first suggestion is that this is perhaps what is happening.
Here is an example:
You start a timer to do something every second.
Immediately after, you start a long running main thread task (for example, you try to write a ton of data to a file). This task will take five seconds to complete.
Your timer wants to fire after one second, but your file-writing is
hogging the main thread for the next four seconds, so the timer can't fire
for another four seconds.
If this is the case, then to solve the problem you would either need to move that main thread work to a background thread, or else figure out a way to do it while returning to the run loop periodically. For example, during your long running main thread operation, you can periodically call runUntilDate on your run loop to let other run loop tasks execute.
Note that you couldn't just increment the progress bar fill periodically during the long running main thread task, because the progress bar will not actually animate its fill until you return to the run loop.
What about proper way for animating changes: animateWithDuration:animations: or CABasicAnimation. You can use this for creating smooth animations

Resources