0

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.

2
  • This .id(requ.questionAndAnswers.count) does not give you a unique id for each Text(qa.answer). You could try using the qa id property instead. Then scroll to the last id of the requ.questionAndAnswers in the .onChange() Commented Mar 6, 2023 at 9:17
  • @workingdog: I tried "qa.id" too. But the same. ScrollView will be append, but not moving to the bottom. Commented Mar 6, 2023 at 15:04

1 Answer 1

4

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
                            }
                        }
                    }
            }
        }
        
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Many thanks. The key was using ".questionAndAnswers.last" instead of count

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.