runemonster
runemonster

Reputation: 199

ScrollView moves content over when scrolling

When using a ScrollViewReader that is passed to another view, the views content moves over when scrolling. If the ForEach is inside the same view as the reader the view shift does not happen. When you manually scroll the views fix themselves. I have posted a simplified version below.

How would I stop the view from shifting over when the view is being scrolled?

I am using Xcode13 beta 5

struct ContentView: View {
    var body: some View {
        VStack {
            
            ScrollView {
                ScrollViewReader { proxy in
                    Button("Scroll to bottom") {
                        withAnimation {
                            proxy.scrollTo(99, anchor: .bottom)
                        }
                    }
                    TestView(proxy: proxy)
                }
            }
        }
    }
}

struct TestView: View {
    var proxy: ScrollViewProxy
    
    var body: some View {
        VStack {            ForEach(1..<100) { index in
                Text("Test \(index)")
                    .id(index)
            }
            Button("Scroll to top") {
                withAnimation {
                    proxy.scrollTo(1, anchor: .top)
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 1

Views: 1375

Answers (1)

George
George

Reputation: 30341

You can't have the views to scroll to, nested in another view.

Solution #1

Remove the VStack:

struct TestView: View {
    let proxy: ScrollViewProxy

    var body: some View {
        ForEach(1..<100) { index in
            Text("Test \(index)")
                .id(index)
        }

        Button("Scroll to top") {
            withAnimation {
                proxy.scrollTo(1, anchor: .top)
            }
        }
    }
}

Solution #2

Give the extracted view the content at the top of the ScrollView, by passing it in:

struct ContentView: View {
    var body: some View {
        VStack {
            TestView { proxy in
                Button("Scroll to bottom") {
                    withAnimation {
                        proxy.scrollTo(99, anchor: .bottom)
                    }
                }
            }
        }
    }
}
struct TestView<Content: View>: View {
    @ViewBuilder let content: (ScrollViewProxy) -> Content

    var body: some View {
        ScrollView {
            ScrollViewReader { proxy in
                content(proxy)

                ForEach(1..<100) { index in
                    Text("Test \(index)")
                        .id(index)
                }

                Button("Scroll to top") {
                    withAnimation {
                        proxy.scrollTo(1, anchor: .top)
                    }
                }
            }
        }
    }
}

I would recommend a slightly different way of doing this. If possible, it is probably best to have this in just one view. However, you want to reuse some of the components so this may not be what you are looking for.

Another thing I noticed: the scrolling doesn't go far enough to reach the buttons, which can appear underneath the safe-area and unable to be pressed. Make these changes:

Button("Scroll to bottom") {
    withAnimation {
        proxy.scrollTo("bottom", anchor: .bottom)
    }
}
.id("top")
Button("Scroll to top") {
    withAnimation {
        proxy.scrollTo("top", anchor: .top)
    }
}
.id("bottom")

Upvotes: 3

Related Questions