Is there a way to observe changes to fractionComplete in UIViewPropertyAnimator
Asked Answered
B

3

27

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.

Bewley answered 9/12, 2016 at 3:3 Comment(5)
Good question, looking for an answer too!Retrace
Since you are updating the UIViewPropertyAnimator somewhere, why don't you just update the slider at the same point?Janijania
@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.Bewley
CADisplayLink is the way to goRosetterosewall
I'm pretty certain that fractionComplete is not a dynamic property, therefore cannot be observed for changes using KVOGrindelia
B
4

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.

Bewley answered 26/12, 2020 at 23:30 Comment(0)
H
1

Here is a UIViewPropertyAnimator subclass that uses CADisplayLink to post updates to a onFractionCompleteUpdated callback.

Usage

let animator = MonitorableUIViewPropertyAnimator(duration: 0.45, dampingRatio: 0.8) {
    // Animation Code
}

animator.onFractionCompleteUpdated = { fractionComplete in
    print("Animation Fraction Complete: \(fractionComplete)")
}

Implementation

/// A animator that allows you to monitor fractionComplete via `onFractionCompleteUpdated`.
/// Useful when a animation has key events that need to happen but the events themselves are not animatable.
/// Example: You need to change the zIndex of two views half way through.
class MonitorableUIViewPropertyAnimator : UIViewPropertyAnimator {
    
    private var displayLink: CADisplayLink?
    private var lastFractionComplete: CGFloat?
    
    /// Called everytime `self.fractionComplete` has a new value.
    var onFractionCompleteUpdated: ((CGFloat) -> ())? = nil {
        didSet {
            if onFractionCompleteUpdated != nil {
                if displayLink == nil {
                    displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
                    displayLink?.add(to: .main, forMode: .common)
                    
                    // Clean up when the animation completes
                    addCompletion { [weak self] _ in
                        self?.displayLink?.invalidate()
                    }
                }
            } else {
                // No longer need display link - clean up
                displayLink?.invalidate()
            }
            
        }
    }
    
    @objc private func handleDisplayLink(_ displayLink: CADisplayLink) {
        // Don't call if the animation is paused or not moving
        if lastFractionComplete != fractionComplete {
            onFractionCompleteUpdated?(fractionComplete)
            lastFractionComplete = fractionComplete
        }
        
        // If the animation stops outside of a normal completion - clean up
        if state == .stopped {
            displayLink.invalidate()
        }
    }
}
Honshu answered 4/12, 2023 at 2:15 Comment(0)
B
0

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

class MyPropertyAnimator : UIViewPropertyAnimator {
    override var fractionComplete: CGFloat {
        didSet {
            print("didSetFraction")
        }
    }
}
Benzoic answered 9/1, 2019 at 15:35 Comment(2)
fractionComplete does not update dynamically -- as such, you won't get intermediate values from here -- only start and end state.Cyclothymia
fractionComplete is not KVO compliant. You need to set up a timer or a CADisplayLink to periodically check it.Tokoloshe

© 2022 - 2024 — McMap. All rights reserved.