SwiftUI闹钟视图动画异常问题求助
Netflix风格提醒动画快速点击异常修复
问题描述
实现类似Netflix应用“New & Hot”页面的提醒动画效果时,当前代码存在以下问题:在默认状态(显示铃铛图标、文本为“Remind Me”)下,点击按钮触发铃铛晃动动画时,若快速再次点击,会短暂显示对勾图标后回到铃铛,但文本仍保持“Reminder Set”而非切换回“Remind Me”。期望实现:快速点击时,取消铃铛动画,不显示对勾图标,同时文本能正确切换。
当前代码:
struct AlarmView: View { @State var alarmSet = false @State var animateBell = false @State var alarmText = "Remind Me" @State var shouldAnimate = false @State private var bellID: UUID = UUID() @State private var timeSinceLastTap = Date.now var body: some View { ZStack { Color.black VStack { Button(action: { // animate only if there is some time elapsed between taps let duration = min(0.3, abs(timeSinceLastTap.timeIntervalSinceNow)) shouldAnimate = duration > 0.2 alarmText = !alarmSet ? "Reminder Set" : "Remind Me" if shouldAnimate { if alarmSet == true { // turn OFF alarm alarmSet.toggle() } else { // animate the bell, then turn ON alarm animateBell.toggle() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { alarmSet.toggle() } } } else { // to cancel animation bellID = UUID() alarmSet.toggle() } timeSinceLastTap = Date.now }, label: { VStack { VStack { if alarmSet { Image(systemName: "checkmark") .font(.system(size: 150)) .foregroundColor(.white) } else { Image(systemName: "bell.fill") .font(.system(size: 150)) .foregroundColor(.white) .id(bellID) .keyframeAnimator(initialValue: Angle.zero, trigger: animateBell) { content, value in content.rotationEffect(value, anchor: .top) } keyframes: { _ in KeyframeTrack { let spring = Spring(mass: 0.5, stiffness: 170, damping: 2.5) SpringKeyframe(Angle.degrees(10), duration: 0.05, spring: spring) SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring) SpringKeyframe(Angle.zero, duration: 0.05, spring: spring, startVelocity: Angle.zero) } } } } .frame(width: 150, height: 150) Text(alarmText) .font(.largeTitle) .foregroundStyle(.gray) } }) .buttonStyle(NoAnimationStyle()) } } .ignoresSafeArea() } } struct NoAnimationStyle: PrimitiveButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .contentShape(Rectangle()) .onTapGesture(perform: configuration.trigger) } }
解决方案
核心问题是快速点击时,之前的异步任务仍会执行导致alarmSet状态错误切换,且文本更新逻辑没有考虑取消动画的场景。修改后的完整代码如下:
struct AlarmView: View { @State var alarmSet = false @State var animateBell = false @State var alarmText = "Remind Me" @State private var bellID: UUID = UUID() @State private var timeSinceLastTap = Date.now // 管理可取消的异步任务,避免错误状态切换 @State private var cancelableTask: Task<Void, Never>? var body: some View { ZStack { Color.black VStack { Button(action: { let duration = abs(timeSinceLastTap.timeIntervalSinceNow) let shouldAnimate = duration > 0.2 // 取消未完成的异步任务,阻止错误状态更新 cancelableTask?.cancel() cancelableTask = nil if shouldAnimate { if alarmSet { // 关闭提醒:直接切换状态和文本 alarmSet.toggle() alarmText = "Remind Me" } else { // 开启动画:更新文本并启动异步状态切换 animateBell.toggle() alarmText = "Reminder Set" cancelableTask = Task { try? await Task.sleep(nanoseconds: 500_000_000) if !Task.isCancelled { alarmSet = true } } } } else { // 快速点击:重置所有状态,终止动画 animateBell = false bellID = UUID() alarmSet = false alarmText = "Remind Me" } timeSinceLastTap = Date.now }, label: { VStack { VStack { if alarmSet { Image(systemName: "checkmark") .font(.system(size: 150)) .foregroundColor(.white) } else { Image(systemName: "bell.fill") .font(.system(size: 150)) .foregroundColor(.white) .id(bellID) .keyframeAnimator(initialValue: Angle.zero, trigger: animateBell) { content, value in content.rotationEffect(value, anchor: .top) } keyframes: { _ in KeyframeTrack { let spring = Spring(mass: 0.5, stiffness: 170, damping: 2.5) SpringKeyframe(Angle.degrees(10), duration: 0.05, spring: spring) SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring) SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring) SpringKeyframe(Angle.zero, duration: 0.05, spring: spring, startVelocity: Angle.zero) } } } } .frame(width: 150, height: 150) Text(alarmText) .font(.largeTitle) .foregroundStyle(.gray) } }) .buttonStyle(NoAnimationStyle()) } } .ignoresSafeArea() // 视图销毁时清理任务,避免内存泄漏 .onDisappear { cancelableTask?.cancel() } } } struct NoAnimationStyle: PrimitiveButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .contentShape(Rectangle()) .onTapGesture(perform: configuration.trigger) } }
关键修改点说明
- 添加可取消任务:用
Task替代DispatchQueue异步回调,快速点击时可直接取消未完成的状态切换,避免alarmSet被错误设置为true - 快速点击逻辑优化:直接重置
alarmSet为false,同步更新文本为“Remind Me”,同时重置铃铛id终止当前动画 - 文本更新逻辑调整:根据操作场景明确设置文本,不再依赖
!alarmSet前置判断,避免状态不同步 - 视图销毁清理:在
onDisappear中取消任务,防止内存泄漏
内容的提问来源于stack exchange,提问作者batman
相关产品推荐
相关产品推荐

