【iOS】UIViewPropertyAnimator的坑和注意点

13,715 阅读7分钟

UIViewPropertyAnimator是iOS中的一个动画类,用于创建和管理视图动画。它提供了一种强大且灵活的方式来创建自定义的交互式动画:可以定义动画的起始状态、结束状态和持续时间,还可以通过控制动画的进度来实现交互性。

但是使用UIViewPropertyAnimator的过程中我发现了挺多坑,很多方法的调用不够安全,例如调用了stopAnimation(false)之后如果不调用finishAnimation(at: xxx)在释放时就会崩溃,又或者直接调用finishAnimation(at: xxx)也会立马崩溃等,这些都是会导致崩溃的坑啊。

虽说API已经有注释说明了,但概括得不够全面,加上这些API内部并没有做任何防护机制,一旦用错直接崩溃处理,即便可能是出于性能优化的考虑,可这给我的感觉就是它做得不够友好。

再说了,也并非所有人都会先一字一句地去理解注释写的啥再使用吧,实践出真知,总要去踩坑的。

为了防止以后忘了再次踩坑,我把个人总结的这一部分使用UIViewPropertyAnimator的坑和注意点在这里分享一下。

坑(Crash)

首当其冲是会导致崩溃的坑,一般都是在某个界面创建了UIViewPropertyAnimator的实例对象,当退出该界面时引发的崩溃:

#1

如果退出该界面时,animator的「状态为active并且暂停中」,或者「状态为stopped」,会直接崩溃!

"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."

大概意思就是animator必须完结后才能销毁,解决方法:

  • 如果animator的「状态为active并且暂停中」,退出界面时,需要调用stopAnimation(true)stopAnimation(false)+finishAnimation(at: xxx)

    • 另外如果pausesOnCompletionfalse,也可以调用startAnimation,因为pausesOnCompletiontrue的效果就是当动画完成时变为暂停状态,而非完结状态。
  • 如果「状态为stopped」,只需也只能调用finishAnimation(at: xxx)

deinit {
    // 1.如果状态为active并且暂停中
    animator.stopAnimation(true)
    // 或者
    animator.stopAnimation(false)
    animator.finishAnimation(at: .current)
    // 或者如果pausesOnCompletion为false
    animator.startAnimation()

    // 2.如果状态为stopped
    animator.finishAnimation(at: .current)
}

PS:stopAnimation(true)效果等同于stopAnimation(false)+finishAnimation(at: .current)

#2

如果animator的状态已经为stopped(调用了stopAnimation(false)),再次调用stopAnimation(true/false),会直接崩溃!

"Animator <UIViewPropertyAnimator(0x600003e24200) [stopped] interruptible> is already stopped!"

不能连续stop!

#3

如果animatorstopped状态下直接调用finishAnimation(at: xxx),会直接崩溃!

"finishAnimationAtPosition: should only be called on a stopped animator!"

📢 注意:如果animator从未开启,也就是状态为inactive(没调用过startAnimation/pauseAnimation,也没设置过fractionComplete),调用stopAnimation(false)+finishAnimation(at: xxx)也会崩溃。

  • 这是因为在未开启状态,调用stopAnimation(true/false)是无效的,状态不会变为stopped,相当于直接调用了finishAnimation(at: xxx),所以才崩溃。

注意点

以下是平时使用UIViewPropertyAnimator的一些注意点,也包括了上面的坑:

#1

animator初始化时状态为inactive,只要「调用过startAnimation」或「调用过pauseAnimation」或「设置过fractionComplete」,状态就变为active,之后动画正在运行、正在暂停、还没结束的情况下状态都为active

#2

调用pauseAnimation会暂停动画,状态还是active,可以修改fractionComplete(动画进度),再次调用startAnimation会继续动画。

#3

调用stopAnimation(true),状态变为inactivefractionComplete变为0并且无法修改,再次调用startAnimation无效,但状态会变为active,而且能修改fractionComplete。【📌animator死了】

#4

调用stopAnimation(false),状态变为stopped,保持动画开启前的fractionComplete(如果调用了startAnimation则是调用前的fractionComplete,不是执行动画过程中变化的fractionComplete)并且无法修改,再次调用startAnimation无效,状态还是stopped。【📌animator死了】

#5

如果已经调用了stopAnimation(false),接着调用stopAnimation(false)stopAnimation(true)就会崩溃,不能连续stop!

#6

动画开启并执行完成后:

  1. 如果pausesOnCompletionfalse,会调用addCompletion的闭包,state会变为inactivefractionComplete变为0,此时无法修改fractionComplete,再次调用startAnimation无效,但状态会变为active,而且能修改fractionComplete。【相当于 #3,📌animator死了】
  2. 如果pausesOnCompletiontrue,不会调用addCompletion的闭包,状态还是activefractionComplete变为1,可以继续修改fractionComplete(动画进度),再次调用startAnimation会继续动画。【相当于完成时调用了pauseAnimation

#7

当状态为active时,app进入后台再回到前台:

  1. 如果pausesOnCompletionfalse,动画会直接完成:会调用addCompletion的闭包,状态变为inactivefractionComplete变为0。【相当于 #6.1,📌animator死了】
  2. 如果pausesOnCompletiontrue,动画会暂停:不会调用addCompletion的闭包,状态还是active,保持当前fractionComplete,再次调用startAnimation会继续动画【相当于 #6.2,animator自动调用了pauseAnimation

#8

如果animator已经启动(状态为active),必须要先stopAnimation(false)才能调用finishAnimation(at: xxx),也就是说只有状态为stopped才能调用finishAnimation(at: xxx),否则直接调用finishAnimation(at: xxx)会直接崩溃!

  1. 调用finishAnimation(at: xxx)会触发addCompletion的闭包,即便pausesOnCompletiontrue也会触发,相当于强制完成动画,
  2. stopAnimation(false)+finishAnimation(at: xxx)等同于调用可以选择最终进度(start、current、end)的stopAnimation(true)。【相当于 #3,📌animator死了】
  3. 相反,stopAnimation(true)等同于stopAnimation(false)+finishAnimation(at: .current)。【📌animator死了】

#9

如果animator的状态是从active变为inactive的(也就是启动后调用了stopAnimation(false)+finishAnimation(at: xxx)stopAnimation(true)),之后怎么调用finishAnimation(at: xxx)都不会有动静,但是,如果接着调用startAnimation,虽然无效,但状态会变为active,之后再调用finishAnimation(at: xxx)就会崩溃。【相当于 #8,非stopped状态下就调用finishAnimation(at: xxx)

#10

如果animator从来没启动过(状态还没变为active),调用stopAnimation(true)stopAnimation(false)是无效的,是可以继续开启动画的。因此如果animator从来没启动,就调用stopAnimation(false)+finishAnimation(at: xxx)是无法移除动画的,而且会崩溃,因为stopAnimation(false)执行无效,接着就调用finishAnimation(at: xxx)。【相当于 #8,非stopped状态下就调用finishAnimation(at: xxx)

关于【📌animator死了】

只要animator启动后再停止,也就是状态先从inactive变为active,再从active变为stoppedinactive后,我都将其视为animator死了

animator一旦死了,将无法再次启动动画,调用任何方法都不会再起作用,所以死了就不要再使用该animator了,免得状态和fractionComplete的错乱导致判断失误(例如上面的 #3、 #6.1、 #9)。

  • 感觉跟自定义Thread一个道理,任务完成后就是一具尸体。

若想启动新动画,应该是直接新建一个animator去使用

在UITableView、UICollectionView上失效的情况

如果在cell上使用了UIViewPropertyAnimator,只要cell被复用过,它的animator就会永久失效(整个app的生命周期内):animator的方法能正常运作,但实际上已经没有动画效果(动画完成的状态),跟死了差不多情况。

🌰🌰🌰 使用animator设置10%模糊度:

jp_gif_file 2.GIF

PS:手动对cell调用prepareForReuse方法同样也会让animator永久失效。

不过可能苹果就是不希望在这些复用列表中使用UIViewPropertyAnimator吧,毕竟对性能会有一定的负担。但是如果非得要用,也不是不可以,只要在cell每次复用后重置一个新的animator即可:

// MARK: - UICollectionViewCell
class JPCell: UICollectionViewCell {
    ......
    
    private let effect = UIBlurEffect(style: .light)
    private let effectView = UIVisualEffectView(effect: nil)
    private var animator: UIViewPropertyAnimator?
    
    ......
    
    deinit {
        removeAnimator()
    }
    
    private func removeAnimator() {
        guard let animator else { return }
        if animator.state == .stopped {
            animator.finishAnimation(at: .current)
        } else {
            animator.stopAnimation(true)
        }
        self.animator = nil
    }
    
    func resetAnimator() {
        removeAnimator()
        
        effectView.effect = nil
        
        let animator = UIViewPropertyAnimator(duration: 10, curve: .linear, animations: { [weak self] in
            self?.effectView.effect = self?.effect
        })
        animator.pausesOnCompletion = true
        animator.fractionComplete = 0.1
        self.animator = animator
    }
}

// MARK: - UICollectionViewDataSource
extension JPCollectionViewController: UICollectionViewDataSource {
    ......
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "JPCell", for: indexPath) as! JPCell

        // 需要在Runloop下一个循环中重置,否则还是会失效。
        DispatchQueue.main.async {
            cell.resetAnimator()
        }

        return cell
    }
    
    ......
}

效果如下:

jp_gif_file 3.GIF

最后

针对以上UIViewPropertyAnimator的坑和注意点,后续将整合起来封装成一个安全的animator,然后同步到我之前写的JPBlurView中去(顺便也适配一下在UITableView、UICollectionView上的情况),如有需要,敬请期待。

That is all, thanks.