SwiftUI 动画并不总是运行

Posted

技术标签:

【中文标题】SwiftUI 动画并不总是运行【英文标题】:SwiftUI Animation Doesn't Always Run 【发布时间】:2020-12-22 22:25:30 【问题描述】:

我正在编写一些代码来自学 SwiftUI,而我目前的项目是一个掷骰子应用程序。

在我看来,我展示了一些使用 SFSymbols 的图像来表示掷骰后每个骰子的值。我在每张图像上使用了两个动​​画(.rotation3Deffect.rotationEffect)来提供骰子的视觉滚动。

我发现动画只会在单个骰子值(或者在代码的情况下,如果Image(systemName: "\(diceValue).square.fill"))从一卷更改为下一卷时运行。换句话说,如果我掷三个骰子,它们落在 1-4-6,然后我再次掷骰子,它们落在 1-4-7,1 和 4 图像将执行动画,但图像从 6到 7 没有动画 - 它只是更新到新图像。知道为什么动画只在与前一次掷骰子没有变化的图像上运行吗?

import SwiftUI
struct Dice 
    var sides: Int
    var values: [Int] 
        var calculatedValues = [Int]()
        for v in 1...sides 
            calculatedValues.append(v)
        
        return calculatedValues
    
    
    func rollDice() -> Int 
        
        let index = Int.random(in: 0..<values.count)
        return values[index]
    


struct Roll: Hashable, Identifiable 
    let id = UUID()
     var diceNumber: Int = 0
     var diceValue: Int = 0


struct DiceThrow: Hashable 
    var rolls = [Roll]()
    
    var totalScore: Int 
        var score = 0
        for roll in rolls 
            score += roll.diceValue
        
        return score
    


class ThrowStore: ObservableObject, Hashable 
    @Published var diceThrows = [DiceThrow]()
    
    static func ==(lhs: ThrowStore, rhs: ThrowStore) -> Bool 
        return lhs.diceThrows == rhs.diceThrows
    
    
    func hash(into hasher: inout Hasher) 
        hasher.combine(diceThrows)
    


class Settings: ObservableObject 
    @Published var dicePerRoll: Int = 1
    @Published var sidesPerDice: Int = 4
    
    init(dicePerRoll: Int, sidesPerDice: Int) 
        self.dicePerRoll = dicePerRoll
        self.sidesPerDice = sidesPerDice
    
    



struct ContentView: View 
    var throwStore = ThrowStore()
    let settings = Settings(dicePerRoll: 3, sidesPerDice: 6)
    @State private var roll = Roll()
    @State private var diceThrow = DiceThrow()
    @State private var isRotated = false
    @State private var rotationAngle: Double = 0
    var animation: Animation 
        Animation.easeOut(duration: 3)
    
    
    var body: some View 
        VStack 
            if self.throwStore.diceThrows.count != 0 
                HStack 
                    Text("For dice roll #\(self.throwStore.diceThrows.count)...")
                        .fontWeight(.bold)
                        .font(.largeTitle)
                        .padding(.leading)
                    Spacer()
                
            
            List 
                HStack(alignment: .center) 
                    Spacer()
                    ForEach(diceThrow.rolls, id: \.self)  printedRoll in
                        Text("\(printedRoll.diceValue)")
                            .font(.title)
                            .foregroundColor(.white)
                            .frame(width: 50, height: 50)
                            .background(RoundedRectangle(cornerRadius: 4))
                            .rotation3DEffect(Angle.degrees(isRotated ? 3600 : 0), axis: (x: 1, y: 0, z: 0))
                            .rotationEffect(Angle.degrees(isRotated ? 3600 : 0))
                            .animation(animation)
                    
                    Spacer()
                
                if self.throwStore.diceThrows.count != 0 
                    HStack 
                        Text("Total for this roll:")
                        Spacer()
                        Text("\(self.diceThrow.totalScore)")
                            .padding(.leading)
                            .padding(.trailing)
                            .background(Capsule().stroke())
                    
                
            
            Spacer()
            Button("Roll the dice") 
                self.diceThrow.rolls = [Roll]()
                self.isRotated.toggle()
                for diceNumber in 1...settings.dicePerRoll 
                    let currentDice = Dice(sides: settings.sidesPerDice)
                    roll.diceNumber = diceNumber
                    roll.diceValue = currentDice.rollDice()
                    
                    self.diceThrow.rolls.append(roll)
                
                
                self.throwStore.diceThrows.append(self.diceThrow)
                
            
                .frame(width: 200)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .clipShape(Capsule())
            .padding(.bottom)
            

        
    
    
    func showDiceRoll(_ sides: Int) 
        
    

【问题讨论】:

请使您的示例可重现。见How to create a Minimal, Reproducible Example 我已经用可以重现问题的代码更新了原始帖子。运行它并多次点击掷骰子按钮。您将看到动画仅在图像未从前一卷更改时运行。我的目标是让动画在每次滚动时为每个骰子运行。 【参考方案1】:

在您的场景中,ForEach 中的数据标识符应该保持不变,否则相应的视图将被替换并且动画不起作用。

解决方法很简单 - 使用 diceNumber 作为 ID:

ForEach(diceThrow.rolls, id: \.diceNumber)  printedRoll in
    Text("\(printedRoll.diceValue)")

使用 Xcode 12.1 / ios 14.1 测试

【讨论】:

以上是关于SwiftUI 动画并不总是运行的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI DatePicker 并不总是更新 State<Date> 变量

如何在 SwiftUI 中点击手势时放大缩小按钮动画?

SwiftUI4.0有趣的动画升级:新iOS16视图内容过渡动画

SwiftUI4.0有趣的动画升级:新iOS16视图内容过渡动画

在 SwiftUI 中动态更改动画的持续时间

ScrollView 的 SwiftUI 动画