How to maintain scroll position in LazyVStack / VStack when insert item at the top of a list

Viewed 1004

I create a simple vertical list that contain items to show.

The problem is when I insert items at the top of the list via insertAtTop(). The scroll view does not stay in the same place.

See the video: https://streamable.com/i7ywab


How can I make the scroll view stay in the same position?

Here is the sample code.

struct TestView: View {
    
    @State private var items: [Item] = []
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack {
                    ForEach(items, id: \._id) { item in
                        Text(item.text)
                            .background(Color.green)
                            .padding(.bottom, 10)
                    }
                }
            }
            .navigationBarItems(trailing: HStack {
                Button("[Insert at top]", action: {
                    insertAtTop()
                })
                Spacer(minLength: 20)
                Button("[Load]", action: {
                    load()
                })
            })
            .navigationTitle("Item List")
        }
    }
    
    private func load() {
        items = (1...50).map { _ in Item() }
    }
    
    private func insertAtTop() {
        let newItems = (1...20).map { _ in Item() }
        items.insert(contentsOf: newItems, at: 0)
    }
    
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

struct Item {
    var _id = UUID()
    var text = UUID().uuidString.prefix(5)
}
1 Answers

The problem only seems to arise if you add new views at the top of the list. If you modify or replace existing views, there isn't a problem.

...so one workaround is to loop your ForEach over a placeholder array with a very large number of views in the negative direction (e.g. 10000). Then in your ForEach, check the index to see whether it exists in the real/non placeholder array.

If so -> the real view If not -> a cheap view (e.g. Rectangle() with the right background colour and a height of 1).

    LazyVStack {
ForEach(placeholderIndices, id: \.self) { index in
    if indices.contains(index) {
    
    REAL VIEW
    
    } else {
    
    Rectangle()
    .fill(Color.bg)
    .frame(height: 0)
    
    }
    }

When you expand your view, placeholderIndices stays the same; only the 'real' indices change

Related