Search code examples
swiftuiswiftui-animation

SwiftUI views inserted with ForEach not updating with animation


I created a simple function that inserts three Buttons. I want an individual button to rotate when pressed. My initial try was this:

ForEach(0..<3, id: \.self) { number in {
    Button(action: {
            self.flagTapped(number: index)
            withAnimation(Animation.interpolatingSpring(stiffness: 15.0, damping: 3.0)) {
                animationAmount2 += 360
            }
    }) {
            Image(self.countries[index])
                 .renderingMode(.original)
    }
    .clipShape(Capsule())
    .overlay(Capsule().stroke(Color.black, lineWidth: 1)
    .shadow(color: .black, radius: 10, x: 3, y: 3)
    .rotation3DEffect(
    .degrees(animationAmount2),
         axis: (x: 0.0, y: 1.0, z: 0.0),
         anchor: .center,
         anchorZ: 0.0,
         perspective: 1.0
    )
 }

It works, but the issue is that every button animates when you press any button because animationAmount2 is an @State property so when it is updated, every button animates, not just the one that was pressed.

My next thought was to create a custom button and insert the animation code and properties inside it, so that the buttons would animate individually. That resulted in:

func getFlagView(index: Int) -> some View {
    
    let flag = CustomButton(country: countries[index], index: index) { (Index) in
        flagTapped(number: index)
    }
    
    return flag
}

I now call this function in the ForEach, and it inserts the buttons perfectly, and only the button that I press rotates. The issue is that when the view refreshes it never redraws the buttons. The ForEach is executing, but it is like it simply ignores the calls to getFlagView.

Adding .id(UUID()) to the end of the CustomButton call fixed that:

func getFlagView(index: Int) -> some View {
    
    let flag = CustomButton(country: countries[index], index: index) { (Index) in
        flagTapped(number: index)
    }.id(UUID())
    
    return flag
}

Now the buttons redraw when the view refreshes as expected, but the animation doesn't work. I am really at loss as to why adding the UUID breaks the animations.


Solution

  • In order for SwiftUI to animate your button, it needs to be able to observe a change between renderings of a uniquely identified view. In your first case, your views had id's 0, 1, and 2. So the animation worked.

    When you applied .id(UUID()), this gave the buttons a unique id every time they are drawn. So SwiftUI doesn't see that you've changed a button because it always sees the 3 buttons as 3 entirely new buttons every time ForEach executes.

    What you need is an id that uniquely identifies each of the buttons, but it doesn't change until the countries change. You need an id that uniquely identifies each country, and that id is the country's name.

    Change your getFlagView to use the country's name as the id:

    func getFlagView(index: Int) -> some View {
        
        let flag = CustomButton(country: countries[index], index: index) { (Index) in
            flagTapped(number: index)
        }.id(countries[index])
        
        return flag
    }