26

I've been looking at the very cool new UIViewPropertyAnimator class in iOS 10. It lets you easily do things like pause, resume, and reverse in-flight UIView animations. It used to be that you had to manipulate the underlying CAAnimations the system created in order to do that with UIView animations.

The new UIViewPropertyAnimator class has a property fractionComplete. It varies from 0.0 (beginning of animation) to 1.0 (end of animation.) If you change the value, you can "scrub" an animation from beginning to end.

I have a demo project on Github called KeyframeViewAnimations that lets you use a slider to scrub UIView and CAAnimations back and forth using fairly arcane CAAnimation tricks. As the animation runs, the slider moves from beginning to end to show the progress of the animation. That project was written in Objective-C before Swift was released, but the underlying techniques are the same in Objective-C or Swift.

I decided to create an equivalent project using UIViewPropertyAnimator. There are some quirks, but it's fairly straightforward. One thing I could not figure out how to do cleanly was to observe the progress of the animation's fractionComplete property so that I could update the slider as the animation progresses. I tried setting up KVO (key-value-observing) on the fractionComplete property, but it doesn't trigger a KVO notification until the animation is complete.

What I ended up doing is setting up a timer that runs on a very small interval and queries the UIViewPropertyAnimator's fractionComplete property repeatedly and updating the slider as it progresses. It works, but it's not a very clean way of doing things. It would be much better if there was a way to get notified about the progress of the animation.

I'm hoping there's something I'm missing.

You can see the UIViewPropertyAnimator based project on Github at this link: UIViewPropertyAnimator-test.

The code that gets the progress value from the animator is this code:

func startTimer(_ start: Bool) {
  if start {
    timer = Timer.scheduledTimer(withTimeInterval: 0.02,
     repeats: true) {
        timer in
        var value = Float(self.viewAnimator.fractionComplete)
        if self.animatingBackwards {
          value = 1 - value
        }
        self.animationSlider.value = value
     }
  }
  else {
    self.timer?.invalidate()
  }
}

I should probably convert the code above to use a CADisplayLink timer instead of a vanilla Timer (NSTimer) but I'm hoping there's a better way.

Blazej SLEBODA
  • 6,503
  • 3
  • 35
  • 69
Duncan C
  • 115,063
  • 19
  • 151
  • 241
  • 1
    Good question, looking for an answer too! – StackUnderflow Jan 15 '17 at 21:45
  • 1
    Since you are updating the `UIViewPropertyAnimator` somewhere, why don't you just update the slider at the same point? – Aline Kolczycki Borges Oct 02 '18 at 13:44
  • @AlineKolczyckiBorges the animation normally runs from beginning to end without my code having to update the `UIViewPropertyAnimator`. I may pause the animation, or scrub it back/forth, but the normal case is that it progresses from beginning to end. I would expect there to be a way to observe changes regardless of how those changes come about. – Duncan C Oct 02 '18 at 15:33
  • 4
    CADisplayLink is the way to go – Sentry.co Oct 17 '18 at 10:19
  • I'm pretty certain that fractionComplete is not a dynamic property, therefore cannot be observed for changes using KVO – henrik-dmg Jan 07 '19 at 14:10

2 Answers2

0

Did you try subclassing UIViewPropertyAnimator and then overriding the fractionComplete property?

class MyPropertyAnimator : UIViewPropertyAnimator {
    override var fractionComplete: CGFloat {
        didSet {
            print("didSetFraction")
        }
    }
}
Kokolo
  • 220
  • 1
  • 11
0

FYI, after researching this, the answer appears to be that no, there is no way to get updates as your animation progresses. You have to set up a timer (Either a Timer or a CADisplayLink and update your progress value each time it fires.

Duncan C
  • 115,063
  • 19
  • 151
  • 241