Better alternative to timer based animation for custom activity indicator? - ios

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
}

Related

View animation sometimes perform in zero time

Sometimes animation perform without animation effect(zero time).
In below code sometimes code print "Time = 0.0000002"
let date = Date()
UIView.animate(withDuration: 0.2, animations: {
}) { (finish) in
print("Time = \(Date().timeIntervalSince(date))")
}
It will because you are not animating anything inside the animations block so this is a normal behavior. You can verify with below code,
let date = Date()
self.view.layer.opacity = 0.5
UIView.animate(withDuration: 2.0, animations:
self.view.layer.opacity = 1.0
}) { (finish) in
print("Time = \(Date().timeIntervalSince(date))")
}
The purpose of animations block is to animate the property's start and end values difference on the given time. Once the property value is reached to the given value in animations block it will immediately call finish block.
If you have nothing to animate and just want a callback after few seconds better to use Timer or DispatchQueue e.g,
Timer(timeInterval: 0.2, repeats: false) { timer in
// Do whatever you want
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
// Do whatever you want
}

'Press and hold' animation without NSTimer

Update: The NSTimer approach works now, but comes with a huge performance hit. The question is now narrowed down to an approach without NSTimers.
I'm trying to animate a 'Press and hold' interactive animation. After following a load of SO answers, I've mostly followed the approach in Controlling Animation Timing by #david-rönnqvist. And it works, if I use a Slider to pass the layer.timeOffset.
However, I can't seem to find a good way to continuously update the same animation on a press and hold gesture. The animation either doesn't start, only shows the beginning frame or at some points finishes and refuses to start again.
Can anyone help with achieving the following effect, without the horrible NSTimer approach I'm currently experimenting with?
On user press, animation starts, circle fills up.
While user holds (not necessarily moving the finger), the animation should continue until the end and stay on that frame.
When user lifts finger, the animation should reverse, so the circle is empties again.
If the user lifts his finger during the animation or presses down again during the reverse, the animation should respond accordingly and either fill or empty from the current frame.
Here's a Github repo with my current efforts.
As mentioned, the following code works well. It's triggered by a slider and does its job great.
func animationTimeOffsetToPercentage(percentage: Double) {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
let timeOffset = maximumDuration * percentage
print("Set animation to percentage \(percentage) with timeOffset: \(timeOffset)")
fillAnimationLayer.timeOffset = timeOffset
}
However, the following approach with NSTimers works, but has an incredible performance hit. I'm looking for an approach which doesn't use the NSTimer.
func beginAnimation() {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset >= 1.0 {
self.layer.timeOffset = self.maximumDuration
}
else {
self.layer.timeOffset += 0.1
}
})
}
func reverseAnimation() {
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset <= 0.0 {
self.layer.timeOffset = 0.0
}
else {
self.layer.timeOffset -= 0.1
}
})
}
When you use slider you use fillAnimationLayer layer for animation
fillAnimationLayer.timeOffset = timeOffset
However, in beginAnimation and reverseAnimation functions you are using self.layer.
Try to replace self.layer.timeOffset with self.fillShapeLayer!.timeOffset in your timer blocks.
The solution is two-fold;
Make sure the animation doesn't remove itself on completion and keeps its final frame. Easily accomplished with the following lines of code;
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The hard part; you have to remove the original animation and start a new, fresh reverse animation that begins at the correct point. Doing this, gives me the following code;
func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double)
{
// Always create a new animation.
let animation: CABasicAnimation = CABasicAnimation(keyPath: "path")
if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation {
// If an animation exists, reverse it.
animation.fromValue = currentAnimation.toValue
animation.toValue = currentAnimation.fromValue
let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
// For the timeSinceStart, we take the minimum from the duration or the time passed.
// If not, holding the animation longer than its duration would cause a delay in the reverse animation.
let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration)
// Now convert for the reverse animation.
let reversePauseTime = currentAnimation.duration - timeSinceStart
animation.beginTime = pauseTime - reversePauseTime
// Remove the old animation
layer.removeAnimationForKey("animation")
// Reset startTime, to be when the reverse WOULD HAVE started.
startTime = animation.beginTime
}
else {
// This happens when there is no current animation happening.
startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
animation.fromValue = startPath
animation.toValue = endPath
}
animation.duration = duration
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
layer.addAnimation(animation, forKey: "animation")
}
This Apple article explains how to do a proper pause and resume animation, which is converted to use with the reverse animation.

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

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

Sprite Kit and Swift, increasing the speed of a node over time?

I have a wall that generates and then moves Left (Pulls) and I'm curious on if I can somehow make it so that overtime it gets faster and faster. Is this possible?
Here is the code I'm using to pull the "wall":
func startMoving() {
let moveLeft = SKAction.moveByX(-300, y: 0, duration: 0.35)
runAction(SKAction.repeatActionForever(moveLeft))
}
This is what Generates the walls in case you need to know:
var generationTimer: NSTimer?
func startGeneratingWallsEvery(seconds: NSTimeInterval) {
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
This line of code is what starts the generator
wallGenerator.startGeneratingWallsEvery(0.5)
everything works, but I want to know how to make it so it starts off slow then gets faster overtime (makes it harder).
One option is to create a variable for the duration of the action and keep a counter that increments every time a wall is pulled. After a certain number of walls have been moved (5 in this example), you can reduce the duration by a certain amount.
var duration: NSTimeInterval = 0.35
var counter: NSUInteger = 0
func startMoving(duration: NSTimeInterval) {
let moveLeft = SKAction.moveByX(-300, y: 0, duration: duration)
runAction(SKAction.repeatActionForever(moveLeft))
counter += 1
if counter % 5 == 0 {
duration -= 0.01
}
}
I'm not sure what will happen if the SKAction duration is negative, so you may need to guard against that.

Resources