1

I'm animating a clock hand that takes a CGFloat value from 0 to 1. While I have the animation, I would like it to be a lot smoother. The total animation takes 5 seconds, as part of an input variable. How can I make this a lot smoother?

Ideally, I'd like to get all the values from 0 to 1 in 5 seconds...

The clock hand does a complete 360 but is a little choppy

@IBAction func start(_ sender: Any) {

    timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(launchTimer), userInfo: nil, repeats: true)

    launchTimer()
}

 func launchTimer()  {

    guard seconds < 4.9 else {

        timer.invalidate()
        seconds = 0

        return
    }

    seconds += 0.1

    clockView.currentPressure = CGFloat(seconds / 5)
    clockView.setNeedsDisplay()

}

EDIT

import UIKit

class GaugeView: UIView {

    var currentPressure : CGFloat = 0.0

    override func draw(_ rect: CGRect) {
        StyleKitName.drawGauge(pressure: currentPressure)
    }
}
Paul S.
  • 1,342
  • 4
  • 22
  • 43

2 Answers2

1

Timer is not appropriate for animations on this scale. 100ms isn't a good step in any case, since it's not a multiple of the frame rate (16.67ms). Generally speaking, you shouldn't try to hand-animate unless you have a specialized problem. See UIView.animate(withDuration:...), which is generally how you should animate UI elements, allowing the system to take care of the progress for you.

For a slightly more manual animation, see CABasicAnimation, which will update a property over time. If you need very manual control, see CADisplayLink, but you almost never need this.

In any case, you must never assume that any timer is called precisely when you ask it to be. You cannot add 0.1s to a value just because you asked to be called in 0.1s. You have to look at what time it really is. Even hard-real-time systems can't promise something will be called at a precise moment. The best you can possibly get is a promise it will be within some tolerance (and iOS doesn't even give you that).


To animate this with UIView (which I recommend), it'll probably be something like:

@IBAction func start(_ sender: Any) {
    self.clockView.currentPressure = 0
    UIView.animate(withDuration: 5, animations: {
         self.clockView.currentPressure = 1
    })
}

With a CABasicAnimation (which is more complicated) it would be something like:

currentPressure = 1 // You have to set it to where it's going or it'll snap back.
let anim = CABasicAnimation(keyPath: "currentPressure")
anim.fromValue = 0
anim.toValue = 1
anim.duration = 5
clockView.addAnimation(anim)
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Rob, thanks for the explanation. How would I use `CABasicAnimation` in my example? – Paul S. Jul 23 '18 at 17:44
  • It depends on what `currentPressure` is. What's the desired range, and does modifying it automatically update the view? – Rob Napier Jul 23 '18 at 18:03
  • See edit above. The desired range is from 0 to 1. Changing the value automatically updates the view... – Paul S. Jul 23 '18 at 18:07
  • The only problem with `UIView.animate(withDuration` is it won't animate from 0 to 1 because `currentPressure` is not an animatable property, like alpha, bounds or frame. – Paul S. Jul 23 '18 at 18:23
  • See https://stackoverflow.com/questions/14192816/create-a-custom-animatable-property for how to make arbitrary values be animatable. An even more explicit tutorial: https://blog.codecentric.de/en/2016/07/custom-property-uiview-block-animation/ – Rob Napier Jul 23 '18 at 18:26
0

Make the time interval smaller to make the animation smoother. That way it will seem like it's gliding around instead of jumping between values.

You can also use spritekit:

import SpriteKit

let wait = SKAction.wait(forDuration: 0.01)
let runAnim = SKAction.run {
    launchTimer()
}
let n = SKNode()
n.run(SKAction.repeat(SKAction.sequence([wait, runAnim]), count: 500))
Tob
  • 985
  • 1
  • 10
  • 26
  • timeInterval: 0.1 is the smallest allowable value. – Paul S. Jul 23 '18 at 15:09
  • Never using SpriteKit before, I'm not sure how this will move the clock arm (update the `clockView.currentPressure` Is this supposed to replace my timer? – Paul S. Jul 23 '18 at 16:37
  • Instead of `timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(launchTimer), userInfo: nil, repeats: true)` add the code above. It serves as a a timer but allows smaller time intervals. – Tob Jul 23 '18 at 16:56
  • You will also want to change `seconds += 0.1` to `seconds += 0.01` (or whatever value to match the value in `SKAction.wait`). – Tob Jul 23 '18 at 16:57
  • `self.launchTimer() never gets called..` – Paul S. Jul 23 '18 at 17:00