Home > front end >  Scope of SwiftUI Animations
Scope of SwiftUI Animations

Time:11-14

Experimenting with SwiftUI animations and it's taxing my old grey matter. I'm getting different results for two very similar variants of code, and can't work out why.

Example: The below produces a vertical stack of three images.

@State private var rotationAmount = 0.0

VStack{        
  ForEach(0..<3) { number in
    Button {
      rotationAmount  = 360
      //execute main functionality
    } label: {
      MyImage(number)
        .rotationEffect(.degrees(rotationAmount), anchor: .center)                        
    }
  }
}

When one of the images (i.e. buttons) is clicked its image spins 360 deg on its Z axis

However if the button action code is changed so it uses a withAnimation block the behaviour is different:

    Button{
      withAnimation {
        rotationAmount  = 360
      }
      //execute main functionality
    } label: {
      MyImage(number)
        .rotationEffect(.degrees(rotationAmount), anchor: .center)                        
    }

With this variation clicking any of the images results in all three of them spinning.

In both cases every button has an animation bound to the rotationAmount property, so I don't understand the difference in behaviour between the two examples. If anything I'd expect all three images to spin in both situations as they are all bounds to the same mutating property.

CodePudding user response:

Each time rotationAmount changes it causes the ContentViews body to get reevaluated. Due to 360 being added to rotationAmount each time, we wouldn't expect there to be any visible changes when there's no animation as the view would be rotated around to the exact position it's in.

To see this you could replace .rotationEffect(.degrees(rotationAmount), anchor: .center) with .font(rotationAmount == 0.0 ? .body : .largeTitle), this will show you that all the views get updated, just some without an animation.

Button {
    rotationAmount  = 360
} label: {
    MyImage(number)
        .font(rotationAmount == 0.0 ? .body : .largeTitle)                      
}

The behavior of Button when being tapped is what is causing one image to rotate with animation without explicitly telling it to do so. Due to the highlight state of the button causing animated changes to the label of the button, animations are also applied to other changes that occur to the button at the same time.

To see that it's caused by the image being in a button label specifically you could use .onTapGesture to see that it won't update when it's not in the button label:

MyImage(number)
    .rotationEffect(.degrees(rotationAmount), anchor: .center)
    .onTapGesture {
        rotationAmount  = 360
    } 

You can also see that it's caused by other animations occurring to the button label at the same time, the code below will cause the rotationEffect change to occur once the button highlighted state has finished animating, resulting in the image not animating when the property changes:

Button {
    DispatchQueue.main.asyncAfter(deadline: .now()   1, execute: {
        rotationAmount  = 360
    })
} label: {
    MyImage(number)
        .rotationEffect(.degrees(rotationAmount), anchor: .center)
}

Here's an example shows that using withAnimation to update one property and not placing the other property change inside withAnimation will cause only the property in the withAnimation block to animate when the button is tapped:

Button {
    rotationAmount  = 360

    withAnimation {
        myColorEnabled.toggle() // @State var myColorEnabled = false
    }
} label: {
    Text("Test")
        .rotationEffect(.degrees(rotationAmount), anchor: .center)
    Text("Hello")
        .foregroundColor(myColorEnabled ? .red : .gray)
}
  • Related