SwiftUI Transactions实现:基于外部状态控制按钮动画启停
解决外部状态控制SwiftUI永久重复动画抖动问题
问题背景
需要实现一个由外部视图状态控制、可条件触发永久重复动画的视图。原内部状态控制的按钮动画正常,但改为外部绑定状态并使用Transactions API时,点击控制按钮出现异常抖动。
原代码回顾
内部状态控制版本(正常工作)
// 内部状态控制的按钮动画代码 struct ButtonBouncerView: View { @State var active: Bool = false var body: some View { Circle() .scaleEffect(active ? 1.08: 1) .animation(Animation.default.repeatForever(autoreverses: true), value: active) .frame(width: 100, height: 100) .onTapGesture { useTransaction() } .onAppear { active = true } } func useTransaction() { var transaction = Transaction() transaction.disablesAnimations = active ? true : false withTransaction(transaction) { withAnimation { active.toggle() } } } }
外部状态控制尝试版本(存在抖动问题)
// 尝试的外部状态控制实现代码 struct OutsideControllableButtonView: View { @State private var active: Bool = false var body: some View { VStack { OutsideControllableButtonScaler(active: $active) Button(action: { // CONTROLLER BUTTON! active.toggle() }) { Text("Control Button") } .padding() .background(Color.green) .foregroundColor(.white) } } } struct OutsideControllableButtonScaler: View { // 激活时按钮执行动画 @Binding var active: Bool var body: some View { Circle() .scaleEffect(active ? 1.08: 1) .animation(Animation.default.repeatForever(autoreverses: true), value: active) .frame(width: 100, height: 100) .onChange(of: active) { newValue in var transaction = Transaction(animation: newValue ? Animation.default.repeatForever(autoreverses: true) : nil) transaction.disablesAnimations = true withTransaction(transaction) { self.active = newValue } } } }
问题原因
- 子视图的
onChange回调中重复修改@Binding的active值,引发循环更新,导致视图反复重绘出现抖动。 - 错误设置
transaction.disablesAnimations = true,与指定的永久重复动画规则冲突,造成动画状态混乱。
修正方案
方案1:外部控制器统一管理动画事务
将动画逻辑移到外部控制按钮中,子视图仅负责响应状态变化:
struct OutsideControllableButtonView: View { @State private var active: Bool = false var body: some View { VStack { OutsideControllableButtonScaler(active: $active) Button(action: { var transaction = Transaction() // 激活时使用永久重复动画,关闭时直接恢复无动画 transaction.animation = active ? nil : Animation.default.repeatForever(autoreverses: true) withTransaction(transaction) { active.toggle() } }) { Text("Control Button") } .padding() .background(Color.green) .foregroundColor(.white) } } } struct OutsideControllableButtonScaler: View { @Binding var active: Bool var body: some View { Circle() .scaleEffect(active ? 1.08: 1) // 依赖外部传入的事务动画更新 .animation(.default, value: active) .frame(width: 100, height: 100) } }
方案2:子视图根据状态自动切换动画
简化外部逻辑,子视图直接根据active状态应用对应动画:
struct OutsideControllableButtonView: View { @State private var active: Bool = false var body: some View { VStack { OutsideControllableButtonScaler(active: $active) Button(action: { active.toggle() }) { Text("Control Button") } .padding() .background(Color.green) .foregroundColor(.white) } } } struct OutsideControllableButtonScaler: View { @Binding var active: Bool var body: some View { Circle() .scaleEffect(active ? 1.08: 1) // 激活时用永久重复动画,关闭时用默认动画恢复 .animation( active ? Animation.default.repeatForever(autoreverses: true) : .default, value: active ) .frame(width: 100, height: 100) } }
效果说明
两种方案都能实现:
- 点击控制按钮激活时,圆形视图开始持续缩放的重复动画
- 再次点击关闭时,视图立即恢复原始大小(或平滑恢复,根据方案选择)
- 无抖动、无异常动画状态
内容的提问来源于stack exchange,提问作者Jack
相关产品推荐
相关产品推荐

