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)
;- 另外如果
pausesOnCompletion
为false
,也可以调用startAnimation
,因为pausesOnCompletion
为true
的效果就是当动画完成时变为暂停状态,而非完结状态。
- 另外如果
-
如果「状态为
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
如果animator
非stopped
状态下直接调用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)
,状态变为inactive
,fractionComplete
变为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
动画开启并执行完成后:
- 如果
pausesOnCompletion
为false
,会调用addCompletion
的闭包,state会变为inactive
,fractionComplete
变为0
,此时无法修改fractionComplete
,再次调用startAnimation
无效,但状态会变为active
,而且能修改fractionComplete
。【相当于 #3,📌animator
死了】 - 如果
pausesOnCompletion
为true
,不会调用addCompletion
的闭包,状态还是active
,fractionComplete
变为1
,可以继续修改fractionComplete
(动画进度),再次调用startAnimation
会继续动画。【相当于完成时调用了pauseAnimation
】
#7
当状态为active
时,app进入后台再回到前台:
- 如果
pausesOnCompletion
为false
,动画会直接完成:会调用addCompletion
的闭包,状态变为inactive
,fractionComplete
变为0
。【相当于 #6.1,📌animator
死了】 - 如果
pausesOnCompletion
为true
,动画会暂停:不会调用addCompletion
的闭包,状态还是active
,保持当前fractionComplete
,再次调用startAnimation
会继续动画【相当于 #6.2,animator
自动调用了pauseAnimation
】
#8
如果animator
已经启动(状态为active
),必须要先stopAnimation(false)
才能调用finishAnimation(at: xxx)
,也就是说只有状态为stopped
才能调用finishAnimation(at: xxx)
,否则直接调用finishAnimation(at: xxx)
会直接崩溃!
- 调用
finishAnimation(at: xxx)
会触发addCompletion
的闭包,即便pausesOnCompletion
为true
也会触发,相当于强制完成动画, stopAnimation(false)
+finishAnimation(at: xxx)
等同于调用可以选择最终进度(start、current、end)的stopAnimation(true)
。【相当于 #3,📌animator
死了】- 相反,
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
变为stopped
或inactive
后,我都将其视为animator
死了,
animator
一旦死了,将无法再次启动动画,调用任何方法都不会再起作用,所以死了就不要再使用该animator
了,免得状态和fractionComplete
的错乱导致判断失误(例如上面的 #3、 #6.1、 #9)。
- 感觉跟自定义Thread一个道理,任务完成后就是一具尸体。
若想启动新动画,应该是直接新建一个animator
去使用。
在UITableView、UICollectionView上失效的情况
如果在cell上使用了UIViewPropertyAnimator
,只要cell被复用过,它的animator
就会永久失效(整个app的生命周期内):animator
的方法能正常运作,但实际上已经没有动画效果(动画完成的状态),跟死了差不多情况。
🌰🌰🌰 使用animator
设置10%模糊度:
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
}
......
}
效果如下:
最后
针对以上UIViewPropertyAnimator
的坑和注意点,后续将整合起来封装成一个安全的animator
,然后同步到我之前写的JPBlurView中去(顺便也适配一下在UITableView、UICollectionView上的情况),如有需要,敬请期待。
That is all, thanks.