Home > Back-end >  ScrollViewReader not scrolling programmatically
ScrollViewReader not scrolling programmatically

Time:09-21

I have the following view and I'm attempting to have it scroll to the bottom on button click as elements are added to the list. I've searched and found that ScrollViewReader is the option to use however my implementation doesn't appear to be working.

My attempts at fixing have included explicitly setting the id of the cell on both the inner views as well as the outer HStack{} I even attempted to set the id to a reference of itself, kind of knowing that's a bad idea, but for brevity. I also removed any extra views inside of the list such as HStack{}, Spacer(), etc.. and just left my ColorsChosenView().id(i) thinking that extra views might cause it, but I digress the issue still persists.

var body: some View {
            VStack {
                ScrollViewReader { reader in
                    List {
                        ForEach(0..<vm.guesses.count, id: \.self) { i in
                            HStack{
                                Spacer()
                                ColorsChosenView(locationCorrect: 1,
                                                 locationIncorrect: 3,
                                                 color1: vm.guesses[i][0],
                                                 color2: vm.guesses[i][1], 
                                                 color3: vm.guesses[i][2], 
                                                 color4: vm.guesses[i][3])
                                Spacer()
                            }.id(i)
                        }
                    }.listStyle(InsetListStyle())

                    Divider()
                        .frame(maxWidth: 250)

                    ColorChoicePicker(vm: vm)

                    Divider()
                        .frame(maxWidth: 250)

                    HStack {
                        Spacer()
                        FABButton(text: "SUBMIT")
                            .onTapGesture {
                                vm.submit()
                                reader.scrollTo(vm.guesses.count - 1)
                            }

                    }.padding()
                }
            }
            .navigationBarHidden(true)
            .navigationBarHidden(true)
            .onAppear(perform: {
                vm.resetGame()
            })
    }

To simplify things, I found that this works just fine. Yet my implementation doesn't feel much different.

var body: some View {
            ScrollViewReader { proxy in
                VStack {
                    Button("Jump to #50") {
                        proxy.scrollTo(50)
                    }

                    List(0..<100, id: \.self) { i in
                        Text("Example \(i)")
                        .id(i)
                    }
                }
            }
        }

CodePudding user response:

Since you're modifying the array, this should work:

1: call the function in the main thread (DispatchQueue.main.async)

-> this will "kinda" work, it will scroll but not to the current but the previous last item

2: (Workaround) handle scrolling in a change-handler (you could also remove the shouldScroll variable if all changes should make it scroll to the bottom)

class NumbersContainer: ObservableObject {
    @Published var numbers: [Int] = Array(0..<25)
    
    func submit() {
        self.numbers.append(self.numbers.count)
    }
}

struct ContentView: View {
    @StateObject var nc = NumbersContainer()
    @State var shouldScroll: Bool = false
    
    var body: some View {
        VStack {
            ScrollViewReader { reader in
                Button("Submit", action: {
                    DispatchQueue.main.async {
                        nc.submit()
                    }
                    self.shouldScroll = true
                })
                
                List {
                    ForEach(0..<nc.numbers.count, id: \.self) { i in
                        HStack {
                            Spacer()
                            Text("Row \(i)")
                            Spacer()
                        }.id(i)
                    }
                }
                .onChange(of: nc.numbers) { newValue in
                    if shouldScroll {
                        reader.scrollTo(newValue.count - 1)
                        shouldScroll = false
                    }
                }
            }
        }
    }
}

Another Possibility would be to use the ScrollReaderProxy as a parameter of the submit function:

class NumbersContainer: ObservableObject {
    @Published var numbers: [Int] = Array(0..<25)
    
    func submit(reader: ScrollViewProxy) {
        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter() // All leaves must have an enter
        DispatchQueue.main.async {
            self.numbers.append(self.numbers.count)
            dispatchGroup.leave() // Notifies the DispatchGroup
        }
        dispatchGroup.notify(queue: .main) {
            reader.scrollTo(self.numbers.count - 1)
        }
    }
}

struct ContentView: View {
    @StateObject var nc = NumbersContainer()
    
    var body: some View {
        VStack {
            ScrollViewReader { reader in
                Button("Submit", action: {
                    nc.submit(reader: reader)
                })
                
                List {
                    ForEach(0..<nc.numbers.count, id: \.self) { i in
                        HStack {
                            Spacer()
                            Text("Row \(i)")
                            Spacer()
                        }.id(i)
                    }
                }
            }
        }
    }
}
  • Related