9

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

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.