App crashing because of paused animation and NSInternalInconsistencyException

Viewed 7591

I animate a UIView on and off the screen by pulling up and swiping down. I use this code to do that:

private var _animator: AnyObject?
@available(iOS 10.0, *)
var animator: UIViewPropertyAnimator? {
    get {
        return _animator as? UIViewPropertyAnimator
    }
    set {
        _animator = newValue
    }
}
var haveTheySwiped = false
var haveTheyNotSwiped = true

@available(iOS 10.0, *)
func handlePan(recognizer: UIPanGestureRecognizer) {
    let vel = recognizer.velocity(in: self)
        switch recognizer.state {
        case .began:
            if vel.y < 0 && !haveTheySwiped {
                animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
                    self.backgroundImage.frame = self.backgroundImage.frame.offsetBy(dx: 0, dy: -603)
                })
                haveTheySwiped = true
                isScanVisible = true
            } else if vel.y > 0 && haveTheySwiped {
                animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
                    self.backgroundImage.frame = self.backgroundImage.frame.offsetBy(dx: 0, dy: 603)
                })
                haveTheySwiped = false
                isScanVisible = false
            }
            animator?.pauseAnimation()
            print(backgroundImage.frame.origin)
        case .changed:
            let translation = recognizer.translation(in: backgroundImage)
            if vel.y < 0 {
                animator?.fractionComplete = translation.y / -603
            } else if vel.y > 0 && haveTheySwiped == true {
                animator?.fractionComplete = translation.y / 603
            }
        case .ended:
            animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
        case .possible: break
        default: break
        }
}

But, recently I have come across a bug. When I try to pull up, the UIView does not appear and then when I lift my finger up, the app crashes with this error:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'It is an error to release a paused or stopped property animator. Property animators must either finish animating or be explicitly stopped and finished before they can be released.'

Has anyone come across this? Would love some help figuring out what is causing this to happen. Any help would be immensely appreciated. Thanks so much in advance!

Cheers, Thoe

7 Answers

In case somebody still gets this crash in 2019 or later, I had a similar issue with an animator I only needed to run for 25% of its fraction complete (to create a subtle blur on a collectionView cell).

The crash would occur when the cell would be deinit or when the VC would be popped from the navController.

Inside the UI setup of my cell, I had the below implementation

Before: leading to crash

 animator?.fractionComplete = 0.25

After: no more crash

 animator?.fractionComplete = 0.25
 animator?.stopAnimation(true) 
 animator?.finishAnimation(at: .current)

This did the trick for me.

The error here is generally due to the fact that the System is trying to clear the animator before it is back to .inactive state.

I don see all your codes.

But generally, something like this will crash

  private func animating(){
    let animation = UIViewPropertyAnimator(duration: 3, curve: .linear) {
        [weak self] in
        self?.view.backgroundColor = .red
    }

    animation.startAnimation()
    animation.pauseAnimation()
}

When the animating function is completed, the system will try to clear the UIViewPropertyAnimator you have created (the property does not live outside of the define scope), but the animation is paused, which means it is still at the .active state (the same goes to animation.stopAnimation(), just that in this case it is .stop state), and like I mention above, anytime when you try to clear the animator before it is back on .inactive state, the system will throw an error.

So, the solution in this case, if you wish to pause or stop animation halfway, you need to find a way to retain the animator.

For example, you can create a property in the viewController like

var animation : UIViewPropertyAnimator!

But you need to make sure that the animator goes back to .inactive state before you allow the system to clear it, or else the crash will just happen again.

Swift 5.x:

animator?.stopAnimation(true)
if let animator = animator, animator.state != .inactive {
    animator.finishAnimation(at: .current)
}

Fixed problem in our case.

I had a similar bug that was happening intermittently. Something like animator.startAnimation(afterDelay: 1) crashed. Further more calling stopAnimation or finishAnimation it may still crash so checking the state of the animator made sure that it won't be called when it shouldn't.

if animator.state == .inactive {
animator.startAnimation(afterDelay: 1) 
}

Depending on your implementation you can handle it differently. However checking for the state of the animator will assure that the command causing the crash will not be called.

May not be exactly of help in the case of the OP but this answer comes up first for similar bugs where the animator is required to be in a particular state.

The state of the animator should be .inactive. To achieve this all you should need is to call animator.stopAnimation(true) at the right moment.

I was using it in a ViewController and had the crash when leaving the screen. I needed to call it just before super.viewDidDisappear(animated).

I had the same issue and combining the knowledge of this question helped me solve the issue:

override func viewWillDisappear(_ animated: Bool) {
    
    articleHeader?.animator.stopAnimation(true)
    
    if let animator = articleHeader?.animator, animator.state != .inactive {
        animator.finishAnimation(at: .current)
    }
}

@programmingbeginner with his input "But you need to make sure that the animator goes back to .inactive state before you allow the system to clear it, or else the crash will just happen again." helped me understand that we need to go back to .inactive before the UIViewController pops from the Navigation stack. The place to do this is viewWillDisappear().

You also need to keep a reference to the animator in your UICollectionView (my animator was in the ArticleHeader view).

@atereshkov: I am using his code to check whether the animator is still active.

To keep reference to the ArticleHeader animator in your UICollectionViewController:

var articleHeader: ArticleHeader?

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    // assign the header here
    articleHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ArticleHeader.reuseIdentifier, for: indexPath) as? ArticleHeader
    
    // do any header configuration here
   
    return articleHeader!
}

Hope it helps someone in the future.

Related