如何使用PhaseAnimator创建不循环回初始阶段的抽奖动画?
实现SwiftUI老虎机式抽奖阶段动画
问题核心
要实现依次展示非中奖奖品、最终停在预先确定中奖奖品的老虎机效果,使用PhaseAnimator时遇到两个问题:
- 默认循环播放阶段,无法停在最后一个中奖阶段
- 调整中奖奖品位置后,结果提前泄露
优雅解决方案
利用PhaseAnimator的阶段序列构造和动画闭包控制,既避免循环,又防止结果提前泄露:
步骤1:明确中奖索引与阶段序列
构造包含完整过渡阶段+最终停留阶段的序列,确保动画结束后停在中奖状态;同时保持中奖奖品在序列末尾,避免提前显示。
步骤2:控制动画逻辑
为非中奖阶段设置过渡动画,最后一个阶段返回nil动画,让视图停留在中奖状态;配合VStack统一管理奖品图标和中奖文本的过渡。
完整代码示例:
import SwiftUI struct LotteryView: View { // 奖品数组,中奖奖品为最后一个元素 let prizes = ["square.and.arrow.up", "pencil", "eraser", "scribble", "trash", "folder"] // 预定义中奖索引 private let winningIndex: Int init() { winningIndex = prizes.count - 1 // 如果是随机中奖,可提前在这里生成随机索引: // winningIndex = Int.random(in: 0..<prizes.count) } // 构造阶段序列:完整遍历所有奖品 + 最后停留在中奖阶段 private let phases: [Int] init(winningIndex: Int) { self.winningIndex = winningIndex phases = Array(0..<prizes.count) + [winningIndex] } @State private var trigger = false var body: some View { PhaseAnimator(phases, trigger: trigger) { currentIndex in VStack(spacing: 20) { // 奖品图标 Image(systemName: prizes[currentIndex]) .font(.system(size: 60)) .id(prizes[currentIndex]) // 确保每个图标触发过渡 .transition(.push(from: .trailing)) // 中奖文本仅在最后阶段显示 if currentIndex == winningIndex { Text("Congratulations! You got \(prizes[currentIndex])!") .font(.title) .transition(.push(from: .bottom)) } } } animation: { currentIndex in // 仅非中奖阶段应用过渡动画,最后阶段无动画(保持停留) currentIndex != winningIndex ? .easeInOut(duration: 0.3) : nil } .onAppear { // 触发一次动画序列 trigger.toggle() } } }
关键优化点
- 避免循环:通过在阶段序列末尾重复中奖索引,并为最后阶段设置
nil动画,让PhaseAnimator执行完所有过渡后停在中奖状态,不会循环回初始阶段。 - 防止提前泄露:中奖奖品保持在序列末尾,仅当动画到达最后阶段时才显示中奖文本,全程不会提前暴露结果。
- 灵活扩展:如果需要随机中奖,只需在初始化时提前生成随机
winningIndex,完全符合“中奖结果在视图展示前已确定”的需求。
替代方案(无需PhaseAnimator)
如果不想使用PhaseAnimator,也可以通过@State变量配合withAnimation和Timer手动控制过渡,但代码复杂度会更高:
struct LotteryViewAlternative: View { let prizes = ["square.and.arrow.up", "pencil", "eraser", "scribble", "trash", "folder"] private let winningIndex = 5 @State private var currentIndex = 0 @State private var isAnimating = false var body: some View { VStack(spacing: 20) { Image(systemName: prizes[currentIndex]) .font(.system(size: 60)) .transition(.push(from: .trailing)) if currentIndex == winningIndex { Text("Congratulations! You got \(prizes[currentIndex])!") .font(.title) .transition(.push(from: .bottom)) } } .onAppear { startAnimation() } } private func startAnimation() { isAnimating = true let totalSteps = prizes.count var currentStep = 0 Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in guard isAnimating else { timer.invalidate() return } withAnimation(.easeInOut) { currentIndex = currentStep % prizes.count } currentStep += 1 if currentStep == totalSteps { // 最后停在中奖索引 withAnimation(.easeInOut(duration: 0.5)) { currentIndex = winningIndex } isAnimating = false timer.invalidate() } } } }
但显然PhaseAnimator的方案更简洁优雅,符合SwiftUI的声明式编程风格。
内容的提问来源于stack exchange,提问作者Sweeper
相关产品推荐
相关产品推荐

