Search code examples
swiftuiviewpropertyanimator

UIViewPropertyAnimator AutoLayout Completion Issue


I'm using UIViewPropertyAnimator to run an array interactive animations, and one issue I'm having is that whenever the I reverse the animations I can't run the animations back forward again.

I'm using three functions to handle the animations in conjunction with a pan gesture recognizer.

private var runningAnimations = [UIViewPropertyAnimator]()

private func startInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {

    if runningAnimations.isEmpty {
        animateTransitionIfNeeded(gestureRecognizer: gestureRecognizer, state: state, duration: duration)
    }

    for animator in runningAnimations {
        animator.pauseAnimation()
        animationProgressWhenInterrupted = animator.fractionComplete
    }
}

private func animateTransitionIfNeeded(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {

    guard runningAnimations.isEmpty else {
        return
    }

    let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {

        switch state {
        case .expanded:
            // change frame
        case .collapsed:
            // change frame
        }
    }

    frameAnimator.isReversed = false

    frameAnimator.addCompletion { _ in
        print("remove all animations")
        self.runningAnimations.removeAll()
    }

    self.runningAnimations.append(frameAnimator)

    for animator in runningAnimations {
        animator.startAnimation()
    }
}

private func updateInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, fractionComplete: CGFloat) {

    if runningAnimations.isEmpty {
        print("empty")
    }
            for animator in runningAnimations {
        animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
    }
}

What I've noticed is after I reverse the animations and then call animateTransitionIfNeeded, frameAnimator is appended to running animations however when I call updateInteractiveTransition immediately after and check runningAnimations, it's empty.

So I'm led to believe that this may have to do with how swift handles memory possibly or how UIViewAnimating completes animations.

Any suggestions?


Solution

  • I've come to realize the issue I was having the result of how UIViewPropertyAnimator handles layout constraints upon reversal. I couldn't find much detail on it online or in the official documentation, but I did find this which helped a lot.

    Animator just animates views into new frames. However, reversed or not, the new constraints still hold regardless of whether you reversed the animator or not. Therefore after the animator finishes, if later autolayout again lays out views, I would expect the views to go into places set by currently active constraints. Simply said: The animator animates frame changes, but not constraints themselves. That means reversing animator reverses frames, but it does not reverse constraints - as soon as autolayout does another layout cycle, they will be again applied.

    Like normal you set your constraints and call view.layoutIfNeeded()

    animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
            [unowned self] in
    
            switch state {
            case .expanded:
                self.constraintA.isActive = false
                self.constraintB.isActive = true
                self.view.layoutIfNeeded()
            case .collapsed:
                self.constraintB.isActive = false
                self.constraintA.isActive = true
                self.view.layoutIfNeeded()
            }
    }
    

    And now, since our animator has the ability to reverse, we add a completion handler to ensure that the correct constraints are active upon completion by using the finishing position.

    animator.addCompletion {  [weak self] (position) in
    
            if position == .start {
                switch state {
                case .collapsed:
                    self?.constraintA.isActive = false
                    self?.constraintB.isActive = true
                    self?.view.layoutIfNeeded()
                case .expanded:
                    self?.constraintA.isActive = false
                    self?.constraintB.isActive = true
                    self?.view.layoutIfNeeded()
                }
            }
    }