Search code examples
swiftuiswiftui-animationswiftui-foreach

Adding items to ForEach and animate the items below nicely so it doesn't jump


I'm trying to get this to animate nicely when I add elements to my array.

Currently the button jumps down after the foreach elements are added.

enter image description here

Here's my code:

import SwiftUI

class ForEachAnimationViewModel: ObservableObject {
    @Published var array: [String] = []
    var count = 5
    
    init() {
        array = (0..<count).map({ "Element \($0)" })
    }
    
    func addMore() {
        let newItems = (count..<count+5).map({ "Element \($0)" })
        array.append(contentsOf: newItems)
    }
}

struct ForEachAnimationView: View {
    @ObservedObject var viewModel: ForEachAnimationViewModel
    var body: some View {
        ScrollView {
            ForEach(viewModel.array, id: \.self) { element in
                Text(element)
            }
            
            Button(action: {
                withAnimation {
                    viewModel.addMore()
                }
            }) {
                Text("Button")
            }
        }
    }
}

#Preview {
    ForEachAnimationView(viewModel: ForEachAnimationViewModel())
}


Solution

  • There is a problem with the way that new entries are being added to the array. The variable count is not being updated when more entries are added, so this results in errors being shown in the console about duplicate ids. One way to fix would be to change the function addMore to base the new ids on the existing count:

    func addMore() {
        let count = array.count
        let newItems = (count..<count+5).map({ "Element \($0)" })
        array.append(contentsOf: newItems)
    }
    

    The animation can then be improved with these changes:

    • add an .animation modifier to the ScrollView
    • add a .transition modifier to the Text items
    • set a minWidth on the Text, to avoid wrapping issues when the width of one row is larger than the width of the previous row.
    struct ForEachAnimationView: View {
        @ObservedObject var viewModel: ForEachAnimationViewModel
        var body: some View {
            ScrollView {
                ForEach(viewModel.array, id: \.self) { element in
                    Text(element)
                        .frame(minWidth: 200)
                        .transition(.opacity)
                }
                Button("Button") {
                    viewModel.addMore()
                }
            }
            .animation(.easeIn(duration: 1), value: viewModel.array)
        }
    }
    

    You can try other types of transition, but if you want to see the rows appear one by one, it would be better to add them one by one instead of in batches of 5.

    Animation