I’m having trouble with a horizontal list in SwiftUI. The list has a conditional that allows it to be displayed or hidden, depending on the state of a toggle. The horizontal list is embedded in a vertical scroll view that contains other views as well, such as an code below, some text, and another horizontal list that is displayed under certain conditions.
The issue is that, when I scroll the first horizontal list and then it disappears because the toggle is turned off, and the second horizontal list appears, the scroll position of the first list is not being saved. I want to be able to save the scroll position of each list, so that when I toggle between them, their respective scroll positions are maintained.
I’ve tried using ScrollViewReader and PreferenceKey to save and restore the scroll position, but it’s not working. I’m not sure if I’m using these APIs correctly or if there’s something else that I’m missing.
Can anyone provide some guidance on how to solve this issue? Any help would be greatly appreciated!
import SwiftUI
struct ContentView: View {
@State private var isList1Visible = true
@State private var scrollPosition1: CGFloat = 0
@State private var scrollPosition2: CGFloat = 0
@State private var previousScrollPosition1: CGFloat = 0
@State private var previousScrollPosition2: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Image(systemName: "person.crop.circle.fill")
.resizable()
.frame(width: 100, height: 100)
.padding()
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed in velit magna. Donec laoreet felis sapien, vel pulvinar orci elementum quis.")
.padding()
// First horizontal list
if isList1Visible {
ScrollViewReader { proxyReader in
ScrollView(.horizontal) {
HStack {
ForEach(0..<10) { index in
Text("(index)")
.frame(width: 100, height: 100)
.background(Color.blue)
.cornerRadius(10)
.id(index)
}
}
.padding(.horizontal, 10)
.background(GeometryReader { proxy in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: proxy.frame(in: .named("vertical_offset")).minX)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollPosition1 = value
}
}
.coordinateSpace(name: "vertical_offset")
.frame(height: 150)
.onDisappear {
previousScrollPosition1 = scrollPosition1
}
}
}
Text("Aliquam eget semper ipsum, quis finibus quam. Nulla facilisi. Praesent rutrum sapien eu tortor commodo fringilla.")
.padding()
// Second horizontal list
if !isList1Visible {
ScrollView(.horizontal) {
HStack {
ForEach(0..<10) { index in
Text("(index)")
.frame(width: 100, height: 100)
.background(Color.green)
.cornerRadius(10)
.id(index)
}
}
.padding(.horizontal, 10)
.background(GeometryReader { proxy in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: proxy.frame(in: .named("vertical_offset_2")).minX)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollPosition2 = value
}
}
.coordinateSpace(name: "vertical_offset_2")
.frame(height: 150)
.onAppear {
scrollPosition2 = previousScrollPosition1
}
.onDisappear {
previousScrollPosition2 = scrollPosition2
}
}
Text("Etiam eget orci dolor. Fusce eu sapien euismod, pharetra est eget, consequat libero. Sed sed tristique nibh.")
.padding()
// Button to toggle the visibility of the horizontal lists
Button(action: {
isList1Visible.toggle()
}) {
Text(isList1Visible ? "Show List 2" : "Show List 1")
}
Spacer()
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
2
Answers
You missed the main idea – this structure (View) will be taken as a code generating source on any changes in data. If your View will change -> the structure will be recreated.
So, if you need to save some states or data – you should create some class "Model" and storage all data in it:
I guess you want this:
Since you want to synchronize the scroll offsets between the two scroll views, which is a feature not offered by SwiftUI’s built-in
ScrollView
as of iOS 16.4, I suggest you wrapUIScrollView
to let you associated aBinding<CGPoint>
with the content offset. Here’s the code, specifically for a horizontally-scrolling scroll view:And here’s the code that I wrapped around it to draw the animated GIF above: