'SwiftUI animation not working using animation(_:value:)

In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.

I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens: So this is not working:

struct ContentView: View {
    @State var isOn = false
    var body: some View {
        Button("Press me") {
            isOn.toggle()
        }
        .animation(.easeIn, value: isOn)
        .frame(width: 300, height: 400)
    }
}

But then this is working. Why?

struct ContentView: View {
    var body: some View {
        Button("Press me") {
        }
        .animation(.easeIn)
        .frame(width: 300, height: 400)
    }
}

The second example animates the button just as the view displays, while the first one does nothing



Solution 1:[1]

The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.

animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.

struct ContentView: View {
    @State var isOn = false
    //The better route is to have a separate variable to control the animations
    // This prevents unpleasant side-effects.
    @State private var animate = false
    
    var body: some View {
        VStack {
            Text("I don't change.")
                .padding()
            Button("Press me, I do change") {
                isOn.toggle()
                animate = false
                // Because .opacity is animated, we need to switch it
                // back so the button shows.
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    animate = true
                }
            }
            // In this case I chose to animate .opacity
            .opacity(animate ? 1 : 0)
            .animation(.easeIn, value: animate)
            .frame(width: 300, height: 400)
            // If you want the button to animate when the view appears, you need to change the value
            .onAppear { animate = true }
        }
    }
}

Solution 2:[2]

Follow up question: animating based on a property of an object is working on the view itself, but when I'm passing that view its data through a ForEach in the parent view, an animation modifier on that object in the parent view is not working. It won't even compile. The objects happen to be NSManagedObjects but I'm wondering if that's not the issue, it's that the modifier works directly on the child view but not on the passed version in the parent view. Any insight would be greatly appreciated

// child view
struct TileView: View {
  @ObservedObject var tile: Tile
var body: some View {
  Rectangle()
    .fill(tile.fillColor)
    .cornerRadius(7)
    .overlay(
      Text(tile.word)
        .bold()
        .font(.title3)
        .foregroundColor(tile.fillColor == .myWhite ? .darkBlue : .myWhite)
    )
     // .animation(.easeInOut(duration: 0.75), value: tile.arrayPos) 
            // this modifier worked here 
}
}

struct GridView: View {
  @ObservedObject var game: Game
  let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)
  var body: some View {
GeometryReader { geo in
  LazyVGrid(columns: columns) {
    ForEach(game.tilesArray, id: \.self) { tile in
      Button(action: {
        tile.toggleSelectedStatus()
        
        moveTiles() <- this changes their array position (arrayPos), and 
             the change in position should be animated
      }) {
        TileView(tile: tile)
          .frame(height: geo.size.height * 0.23)
      }
      .disabled(tile.status == .solved || tile.status == .locked)
      .animation(.easeInOut(duration: 0.75), value: arrayPos) 
      .zIndex(tile.status == .locked ? 1 : 0)
    }
  }
}
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Jessie