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)
}
}
}