如何在SwiftUI中按定时周期交替切换三种不同动画?
多阶段可变时长动画序列的最优实现方案
针对你需要实现的A/B/C交替循环、A时长递增、支持中途取消的动画需求,这里提供一套基于ObservableObject+DispatchWorkItem的结构化实现方案,比零散使用WorkItem更易维护和扩展。
核心思路
- 明确阶段状态:用枚举定义动画的不同阶段(空闲、显示A、显示B、显示C),避免单一布尔值带来的状态模糊问题
- 动态调度任务:通过递归调度每个阶段的结束事件,自动切换到下一个阶段,同时在C阶段完成后递增A的时长
- 可取消机制:用
DispatchWorkItem管理每个阶段的延迟任务,停止时直接取消pending任务并重置状态
ViewModel实现(核心逻辑)
ViewModel负责管理动画状态、计算时长、调度阶段切换,所有状态通过@Published通知View更新:
import Foundation import Combine class AnimationSequenceViewModel: ObservableObject { // 定义动画阶段 enum AnimationPhase { case idle case showA case showB case showC } @Published var currentPhase: AnimationPhase = .idle @Published var isRunning: Bool = false private var currentADuration: TimeInterval = 30 // A初始时长 private let fixedBDuration = 10.0 private let fixedCDuration = 10.0 private var currentWorkItem: DispatchWorkItem? private let taskQueue = DispatchQueue(label: "com.animation.sequence.queue") // 启动动画序列 func startSequence() { guard !isRunning else { return } isRunning = true currentADuration = 30 // 重置初始时长 scheduleNextPhase(.showA) } // 停止动画序列 func stopSequence() { isRunning = false currentWorkItem?.cancel() currentWorkItem = nil currentPhase = .idle } // 递归调度下一个阶段 private func scheduleNextPhase(_ targetPhase: AnimationPhase) { guard isRunning else { return } currentPhase = targetPhase // 计算当前阶段时长和下一个阶段 let duration: TimeInterval let nextPhase: AnimationPhase switch targetPhase { case .showA: duration = currentADuration nextPhase = .showB case .showB: duration = fixedBDuration nextPhase = .showC case .showC: duration = fixedCDuration currentADuration += 10 // 下一轮A时长+10秒 nextPhase = .showA case .idle: return } // 创建并调度任务 let workItem = DispatchWorkItem { [weak self] in self?.scheduleNextPhase(nextPhase) } currentWorkItem = workItem taskQueue.asyncAfter(deadline: .now() + duration, execute: workItem) } }
View层适配
View根据ViewModel的currentPhase状态控制每个动画的显示/隐藏和动画效果,确保动画时长与阶段时长同步:
import SwiftUI struct AnimationSequenceView: View { @StateObject private var viewModel = AnimationSequenceViewModel() var body: some View { VStack { ZStack { // 动画A:圆形淡入淡出 Circle() .fill(.blue) .frame(width: 100, height: 100) .opacity(viewModel.currentPhase == .showA ? 1 : 0) .animation(.easeInOut(duration: viewModel.currentADuration), value: viewModel.currentPhase) // 动画B:三角形偏移 Triangle() .fill(.red) .frame(width: 100, height: 100) .offset( x: viewModel.currentPhase == .showB ? 100 : 0, y: viewModel.currentPhase == .showB ? 100 : 0 ) .animation(.easeInOut(duration: viewModel.fixedBDuration), value: viewModel.currentPhase) // 动画C:三角形缩放 Triangle() .fill(.green) .frame( width: viewModel.currentPhase == .showC ? 100 : 0, height: viewModel.currentPhase == .showC ? 100 : 0 ) .animation(.easeInOut(duration: viewModel.fixedCDuration), value: viewModel.currentPhase) } .padding() // 控制按钮 Button(action: { viewModel.isRunning ? viewModel.stopSequence() : viewModel.startSequence() }) { Text(viewModel.isRunning ? "停止动画序列" : "启动动画序列") .padding() .background(.black) .foregroundColor(.white) .cornerRadius(8) } } } } // 自定义三角形形状 struct Triangle: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.midX, y: rect.minY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) path.closeSubpath() return path } }
关键细节说明
- 状态隔离:ViewModel完全接管动画逻辑,View只负责UI渲染,符合MVVM架构
- 取消安全性:停止时取消当前pending的
DispatchWorkItem,避免任务继续执行导致状态异常 - 动画同步:每个动画的时长直接绑定到对应阶段的时长,确保动画播放时长与阶段停留时长一致
- 扩展性:如果后续需要修改时长规则或新增动画阶段,只需修改
AnimationPhase枚举和调度逻辑即可
内容的提问来源于stack exchange,提问作者ProgrammerTrying
相关产品推荐
相关产品推荐

