SwiftUI ScrollViewReader preserve scroll position on resize

607 views Asked by At

When using a ScrollViewReader to scroll to a certain position e.g. to 40% of the width - how can you preserve the scroll position when the content of the ScrollView resizes?

I wrote a small sample app to illustrate the problem:

Initially, the width of the child of the ScrollView is 1600 and the scroll position is at 40%. When you click the button "Change width to 800" the child width changes to 800 and the ScrollView scrolls to the end. I would like the ScrollView to preserve the scroll position across resizes or to always scroll to 40% after the resize.

struct ContentView: View {

    @State private var relativeScrollPosition: Double?
    @State private var childWidth: CGFloat = 1600


    var body: some View {
        VStack {
            Button("Change width to 800") {
                childWidth = 800
            }
            Button("Change width to 1600") {
                childWidth = 1600
            }
            RelativeScrollView(
                relativeScrollPosition: $relativeScrollPosition,
                childWidth: childWidth
            ) {
                HStack{
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("1")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("2")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("3")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("4")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("5")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("6")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("7")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("8")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("9")
                    }
                    ZStack {
                        Spacer().frame(height: 100).background(Color.green)
                        Text("10")
                    }
                }
                .frame(width: childWidth, height: 100, alignment: .center)
            }
            .onAppear {
                scrollTo40percent()
            }
            .onChange(of: childWidth, perform: { _ in
                scrollTo40percent()
            })
        }
    }

    private func scrollTo40percent() {
        relativeScrollPosition = 0.4
    }
}


struct RelativeScrollView<Content: View>: View {

    @Binding var relativeScrollPosition: Double?
    let childWidth: CGFloat
    let isAnimating = true
    var child: () -> Content

    var body: some View {
        ScrollViewReader { reader in
            ScrollView(.horizontal) {
                ZStack {
                    HStack {
                        // swiftlint:disable identifier_name
                        ForEach(0..<101) { i in
                            Spacer().id(i)
                        }
                    }
                    .frame(width: childWidth)
                    self.child()
                }
            }
            .onAppear {
                scroll(reader, to: relativeScrollPosition)
            }
            .onChange(of: relativeScrollPosition) { newPos in
                scroll(reader, to: newPos)
            }
        }
    }

    private func scroll(_ reader: ScrollViewProxy, to position: Double?) {
        guard let unwrappedPosition = position else { return }
        assert(unwrappedPosition >= 0 && unwrappedPosition <= 1)
        let elementToScrollTo = Int(unwrappedPosition * 100)
        if isAnimating {
            withAnimation {
                reader.scrollTo(elementToScrollTo, anchor: .center)
            }
        } else {
            reader.scrollTo(elementToScrollTo, anchor: .center)
        }
    }
}
0

There are 0 answers