Bart van Kuik
Bart van Kuik

Reputation: 4862

SwiftUI ForEach animation overrides "local" animation

I have a view with an infinite animation. These views are added to a VStack, as follows:

struct PanningImage: View {
    let systemName: String
    @State private var zoomPadding: CGFloat = 0

    var body: some View {
        VStack {
            Spacer()
            Image(systemName: self.systemName)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .padding(.leading, -100 * self.zoomPadding)
                .frame(maxWidth: .infinity, maxHeight: 200)
                .clipped()
                .padding()
                .border(Color.gray)
                .onAppear {
                    let animation = Animation.linear.speed(0.5).repeatForever()
                    withAnimation(animation) {
                        self.zoomPadding = abs(sin(zoomPadding + 10))
                    }
                }
            Spacer()
        }
        .padding()
    }
}

struct ContentView: View {
    @State private var imageNames: [String] = []

    var body: some View {
        NavigationView {
            ScrollView {
                VStack {
                    ForEach(self.imageNames, id: \.self) { imageName in
                        PanningImage(systemName: imageName)
                    }
                    // Please uncomment to see the problem
//                    .animation(.default)
//                    .transition(.move(edge: .top))
                }
            }
            .toolbar(content: {
                Button("Add") {
                    self.imageNames.append("photo")
                }
            })
        }
    }
}

Observe how adding a row to the VStack can be animated, by uncommenting the lines in ContentView.

The problem is that if an insertion into the list is animated, the "local" infinite animation no longer works correctly. My guess is that the ForEach animation is applied to each child view, and somehow these animations influence each other. How can I make both animations work?

Upvotes: 0

Views: 245

Answers (1)

Yrb
Yrb

Reputation: 9725

The issue is using the deprecated form of .animation(). Be careful ignoring deprecation warnings. While often they are deprecated in favor of a new API that works better, etc. This is a case where the old version was and is, broken. And what you are seeing is as a result of this. The fix is simple, either use withAnimation() or .animation(_:value:) instead, just as the warning states. An example of this is:

struct ContentView: View {
    
    @State private var imageNames: [String] = []
    @State var isAnimating = false // You need another @State var
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack {
                    ForEach(self.imageNames, id: \.self) { imageName in
                        PanningImage(systemName: imageName)
                    }
                    // Please uncomment to see the problem
                    .animation(.default, value: isAnimating) // Use isAnimating
                    .transition(.move(edge: .top))
                }
            }
            .toolbar(content: {
                Button("Add") {
                    imageNames.append("photo")
                    isAnimating = true // change isAnimating here
                }
            })
        }
    }
}

The old form of .animation() had some very strange side effects. This was one.

Upvotes: 1

Related Questions