'ScrollView doesn't maintain position when pre-pending elements

I'm building a chat app in SwiftUI using a ScrollView and a ForEach statement. Clearly the chat app needs to scroll to the bottom when the view first loads, so the latest message is shown.

As part of the chat app, I'm only loading 20 elements in view. As the user scrolls up, I'm retrieving more messages and pre-pending them to the relevant array. However, when I do this, the ScrollView scrolls to the top automatically, instead of maintaining it's position.

What do I need to adjust to get this to work?

import SwiftUI


struct ListModel {
    var id = UUID().uuidString
    var text: String
}



class ListViewModel : ObservableObject {

    @Published var items: [ListModel] = [
        ListModel(text: "Item 1"),
        ListModel(text: "Item 2"),
        ListModel(text: "Item 3"),
        ListModel(text: "Item 4"),
        ListModel(text: "Item 5"),
        ListModel(text: "Item 6"),
        ListModel(text: "Item 7"),
        ListModel(text: "Item 8"),
        ListModel(text: "Item 9"),
        ListModel(text: "Item 10"),
        ListModel(text: "Item 11"),
        ListModel(text: "Item 12"),
        ListModel(text: "Item 13"),
        ListModel(text: "Item 14"),
        ListModel(text: "Item 15"),
        ListModel(text: "Item 16"),
        ListModel(text: "Item 17"),
        ListModel(text: "Item 18"),
        ListModel(text: "Item 19"),
        ListModel(text: "Item 20")
    ]


    @Published var prependList: [ListModel] = [
        ListModel(text: "Item -20"),
        ListModel(text: "Item -19"),
        ListModel(text: "Item -18"),
        ListModel(text: "Item -17"),
        ListModel(text: "Item -16"),
        ListModel(text: "Item -15"),
        ListModel(text: "Item -14"),
        ListModel(text: "Item -13"),
        ListModel(text: "Item -12"),
        ListModel(text: "Item -11"),
        ListModel(text: "Item -10"),
        ListModel(text: "Item -9"),
        ListModel(text: "Item -8"),
        ListModel(text: "Item -7"),
        ListModel(text: "Item -6"),
        ListModel(text: "Item -5"),
        ListModel(text: "Item -4"),
        ListModel(text: "Item -3"),
        ListModel(text: "Item -2"),
        ListModel(text: "Item -1")
    ]

}



struct ContentView: View {

    @StateObject var listViewModel = ListViewModel()

    @State var prependListCalled = false

    @State var onAppearCalled = false

    var body: some View {

        ScrollViewReader { scrollView in

            ScrollView {

                LazyVStack {

                    ForEach(listViewModel.items, id: \.id) { item in

                        Text(item.text)
                            .padding(.vertical, 20)

                            .onAppear {
                                if item.id == listViewModel.items.first!.id && prependListCalled == false && onAppearCalled == true {
                                    listViewModel.items.insert(contentsOf: listViewModel.prependList, at: 0)
                                    prependListCalled = true
                                }
                            }
                    }
                }
            }
            .onAppear {
                scrollView.scrollTo(listViewModel.items.last!.id, anchor: .bottom)
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    onAppearCalled = true
                }
            }
        }
        .environmentObject(listViewModel)
    }
}

As a test, I've checked if appending to the array works and the ScrollView position is maintained. This seems to work as expected. See code below which shows that if I load the view without automatically scrolling to the bottom, then scroll manually to the bottom, the array gets appended and the ScrollView position is maintained.

import SwiftUI


struct ListModel {
    var id = UUID().uuidString
    var text: String
}



class ListViewModel : ObservableObject {

    @Published var items: [ListModel] = [
        ListModel(text: "Item 1"),
        ListModel(text: "Item 2"),
        ListModel(text: "Item 3"),
        ListModel(text: "Item 4"),
        ListModel(text: "Item 5"),
        ListModel(text: "Item 6"),
        ListModel(text: "Item 7"),
        ListModel(text: "Item 8"),
        ListModel(text: "Item 9"),
        ListModel(text: "Item 10"),
        ListModel(text: "Item 11"),
        ListModel(text: "Item 12"),
        ListModel(text: "Item 13"),
        ListModel(text: "Item 14"),
        ListModel(text: "Item 15"),
        ListModel(text: "Item 16"),
        ListModel(text: "Item 17"),
        ListModel(text: "Item 18"),
        ListModel(text: "Item 19"),
        ListModel(text: "Item 20")
    ]


    @Published var appendList: [ListModel] = [
        ListModel(text: "Item 21"),
        ListModel(text: "Item 22"),
        ListModel(text: "Item 23"),
        ListModel(text: "Item 24"),
        ListModel(text: "Item 25"),
        ListModel(text: "Item 26"),
        ListModel(text: "Item 27"),
        ListModel(text: "Item 28"),
        ListModel(text: "Item 29"),
        ListModel(text: "Item 30"),
        ListModel(text: "Item 31"),
        ListModel(text: "Item 32"),
        ListModel(text: "Item 33"),
        ListModel(text: "Item 34"),
        ListModel(text: "Item 35"),
        ListModel(text: "Item 36"),
        ListModel(text: "Item 37"),
        ListModel(text: "Item 38"),
        ListModel(text: "Item 39"),
        ListModel(text: "Item 40")
    ]

}




struct ContentView: View {

    @StateObject var listViewModel = ListViewModel()

    @State var appendListCalled = false

    var body: some View {

        ScrollView {

            LazyVStack {

                ForEach(listViewModel.items, id: \.id) { item in

                    Text(item.text)
                        .padding(.vertical, 20)

                        .onAppear {
                            if item.id == listViewModel.items.last!.id && appendListCalled == false {
                                listViewModel.items.append(contentsOf: listViewModel.appendList)
                                appendListCalled = true
                            }
                        }
                }
            }
        }
        .environmentObject(listViewModel)
    }
}


Sources

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

Source: Stack Overflow

Solution Source