UIView动画完成回调执行顺序异常原因及优化方案咨询
嗨,我来帮你拆解这个UIKit动画的问题~
1. 为什么第一个动画的完成回调会最后执行?
UIKit的动画本质是把任务添加到主队列异步执行的,而动画的完成回调会绑定原动画的时长——只要原动画被触发,不管中途有没有新动画打断它,回调都会在原动画的时长到期后,被加入主队列等待执行。
具体到你的场景:
- 当你调用
setMyButtonsVisible:NO后立刻调用setMyButtonsVisible:YES,新的show动画会直接中断当前的hide动画,UIKit会让按钮的alpha立刻跳转到hide动画的当前中间状态(比如执行了0.05秒时的0.5),然后从这个状态开始执行show动画。 - 但之前hide动画的完成回调并没有被取消,它会在自己原本的动画时长(比如0.1秒)结束后,依然执行——哪怕此时show动画已经在运行甚至完成。这就导致hide的回调最后执行,把按钮又设为
hidden=YES,覆盖了show动画的效果。
简单来说:UIView的动画回调是和原动画绑定的,不是和视图的最终状态绑定的。
2. 更优的丢弃重叠动画的方案
这里有几种成熟的解决方案,从简单到进阶:
方案一:取消旧动画并同步当前状态(兼容所有iOS版本)
在执行新动画前,先取消按钮上的所有旧动画,同步视图属性到当前实际显示状态,避免旧回调干扰。修改后的代码如下:
// 先给ViewController加一个属性记录目标状态 @property (nonatomic, assign) BOOL myButtonsVisible; - (void)setMyButtonsVisible:(BOOL)visible { _myButtonsVisible = visible; // 1. 取消按钮上的所有残留动画 [_btn1.layer removeAllAnimations]; [_btn2.layer removeAllAnimations]; // 2. 同步alpha到当前实际显示的状态(从presentationLayer获取实时值) _btn1.alpha = [(NSNumber *)[_btn1.layer.presentationLayer valueForKey:@"alpha"] floatValue]; _btn2.alpha = [(NSNumber *)[_btn2.layer.presentationLayer valueForKey:@"alpha"] floatValue]; if( visible ) { _btn1.hidden = NO; _btn2.hidden = NO; [UIView animateWithDuration:0.2 animations:^{ _btn1.alpha = 1.0; _btn2.alpha = 1.0; }]; } else { [UIView animateWithDuration:0.2 animations:^{ _btn1.alpha = 0.0; _btn2.alpha = 0.0; } completion:^(BOOL finished) { // 校验当前目标状态,避免旧回调误操作 if (!self.myButtonsVisible) { _btn1.hidden = YES; _btn2.hidden = YES; } }]; } }
方案二:使用UIViewPropertyAnimator(iOS 10+)
UIViewPropertyAnimator是苹果推出的更灵活的动画控制器,支持暂停、取消、重定向动画,能从根源上避免回调冲突。
先添加一个属性保存当前动画器:
@property (nonatomic, strong) UIViewPropertyAnimator *currentAnimator; @property (nonatomic, assign) BOOL myButtonsVisible;
然后修改方法:
- (void)setMyButtonsVisible:(BOOL)visible { _myButtonsVisible = visible; // 1. 取消正在运行的旧动画 if (self.currentAnimator && self.currentAnimator.isRunning) { [self.currentAnimator stopAnimation:YES]; [self.currentAnimator finishAnimationAtPosition:UIViewAnimatingPositionCurrent]; } if (visible) { _btn1.hidden = NO; _btn2.hidden = NO; self.currentAnimator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.2 delay:0 options:0 animations:^{ _btn1.alpha = 1.0; _btn2.alpha = 1.0; } completion:^(UIViewAnimatingPosition finalPosition) { self.currentAnimator = nil; }]; } else { self.currentAnimator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.2 delay:0 options:0 animations:^{ _btn1.alpha = 0.0; _btn2.alpha = 0.0; } completion:^(UIViewAnimatingPosition finalPosition) { if (finalPosition == UIViewAnimatingPositionEnd && !self.myButtonsVisible) { _btn1.hidden = YES; _btn2.hidden = YES; } self.currentAnimator = nil; }]; } }
这种方法可以精准控制动画生命周期,旧动画被取消后,它的回调不会再执行。
方案三:优化临时修复(状态校验)
如果你不想改动太多现有代码,可以把状态校验做得更规范:
@property (nonatomic, assign) BOOL myButtonsVisible; - (void)setMyButtonsVisible:(BOOL)visible { _myButtonsVisible = visible; // 先更新目标状态 if( visible ) { _btn1.hidden = NO; _btn2.hidden = NO; [UIView animateWithDuration:0.2 animations:^{ _btn1.alpha = 1.0; _btn2.alpha = 1.0; }]; } else { [UIView animateWithDuration:0.2 animations:^{ _btn1.alpha = 0.0; _btn2.alpha = 0.0; } completion:^(BOOL finished) { // 只有当前目标状态确实是隐藏,才执行操作 if (!self.myButtonsVisible) { _btn1.hidden = YES; _btn2.hidden = YES; } }]; } }
这个方法简单易行,通过状态校验避免错误的回调操作。
内容的提问来源于stack exchange,提问作者olha
相关产品推荐
相关产品推荐

