macosswiftuiscrollviewscrollviewreader

How to automatically scroll in SwiftUI View, if a new item appears?


I have a ScrollView inside a VStack, that will be filled and refreshed each time a new array item appearing. This array containing two sections (qa.question and qa.answer) and was created in a function outside the view. The ScrollView will be filled with these array items as text. I want to automatically scroll down to the bottom, each time a new array will be displayed.

The ScrollView looks like:

ScrollView(showsIndicators: false) {
           ForEach(requ.questionAndAnswers) { qa in
                VStack(spacing: 2) {
                     Text(qa.question)
                         .bold()
                         .foregroundColor(colorScheme == .dark ? .white : .black)
                         .frame(minWidth: 600, maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                      Text(qa.answer)
                         .foregroundColor(colorScheme == .dark ? .white : .black)
                         .padding([.bottom], 10)
                         .frame(minWidth: 600, maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                          .id(requ.questionAndAnswers.count) // give each answer a unique id
                                    }
                                            }
                
                    }.padding([.top, .leading, .trailing], 50)

To find the last item for scrolling, i give Text(qa.answer) the id of array.count. With "print()" i can see, the counter increase each time a new array item appears. Now i embed the whole ScrollView into a ScrollViewReader and try to perform a scroll to the bottom (the requ.questionAndAnswer.count) :

ScrollViewReader { scrollView in
    ScrollView(showIndicators: false) {
....
    }.padding([.top, .leading, .trailing], 50)
     .onChange(of: requ.questionAndAnswers.count) { _ in
                        withAnimation {
                            proxy.scrollTo(requ.questionAndAnswers.count - 1)
                        }
                    }
}

But nothing happened. I tried different version (.onChange, onAppear), make the performing action asynchronous with "DispatchQueue.main.async", or to giving time with "DispatchQueue.main.asyncAfter". No chance.

Any advice would be predicated.


Solution

  • Try this approach, to ... automatically scroll down to the bottom, each time a new array will be displayed. The example code "attach" the .id(qa.id) to the VStack. When a new element is added to the array (using the test button), the ScrollViewReader proxy is scrolled to the last item, based the qa.id.

    // for testing
    class Questioner: ObservableObject {
        // 50 QA
        @Published var questionAndAnswers = Array(repeating: QA(question: "q1", answer: "a1"), count: 50)
    }
    // for testing
    struct QA: Identifiable {
        let id = UUID()
        var question: String
        var answer: String
    }
    
    struct ContentView: View {
        @StateObject var requ = Questioner() // for testing
    
        var body: some View {
            VStack {
                // for testing, adding one more QA
                Button("add one"){
                    requ.questionAndAnswers.append(QA(question: "q-last", answer: "a-last"))
                }.buttonStyle(.bordered)
                
                ScrollViewReader { proxy in  // <-- here proxy not scrollView
                    
                    ScrollView(showsIndicators: false) {
                        ForEach(requ.questionAndAnswers) { qa in
                           VStack(spacing: 12) {
                                Text(qa.question).foregroundColor(.blue).bold()
                                Text(qa.answer).foregroundColor(.red)
                                Divider()
                            }.id(qa.id)  // <-- here
                         }
                        
                    }.padding([.top, .leading, .trailing], 50)
                        .onChange(of: requ.questionAndAnswers.count) { _ in
                            if let last = requ.questionAndAnswers.last {
                                withAnimation {
                                    proxy.scrollTo(last.id)  // <-- here
                                }
                            }
                        }
                }
            }
            
        }
    }