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

1.1k Views Asked by At

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.

1

There are 1 best solutions below

1
On BEST ANSWER

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
                            }
                        }
                    }
            }
        }
        
    }
}