I am using macOS.
I have multiple LazyVGrid in my ScrollView. I am using .scrollPosition() to track the scroll position. Without using this modifier, I don't have any issues, but if, the scrolling is broken.
The problem is similiar to this one: Add multiple LazyVGrid to ScrollView
Unfortunately, I cannot use this solution, because my Grids definitely have different heights and this will cause the problem.
Here is an example code:
struct ContentView: View {
@State var scrollPosition: UUID? = nil
struct MyStruct: Identifiable {
let id = UUID()
let text: String
let uneven: Bool
}
@State var elements: [MyStruct]
init() {
var el = [MyStruct]()
for i in 1..<30 {
el.append(MyStruct(text: "Item\(i)", uneven: i % 2 == 0))
}
self.elements = el
}
var body: some View {
VStack {
ScrollView(.vertical) {
LazyVStack {
ForEach(self.elements) { element in
VStack {
Text(element.text)
LazyVGrid(columns: Array(repeating: GridItem(.fixed(30)), count: 7), content: {
ForEach(0..<7 * (element.uneven == true ? 6 : 7), id: \.self) { index in
Text("\(index)")
}
})
.padding(.bottom, 20)
}
}
} // end LazVStack
.scrollTargetLayout()
.frame(width: 600)
}
.scrollPosition(id: self.$scrollPosition)
}
}
}
You will experience the problem when you completely scroll down the list and then scroll up. Scrolling up is not possible anymore.
If you remove the .scrollPosition-modifier, it works. If you change the ForEach line to this:
ForEach(0..<7 * 7, id: \.self) { index in
it will work too, because then every Grid has the same height.
Adding a fake row will solve the problem, but it will also leave some extra space. Adding a negative padding or offset will not help, it will cause the problem again.
I am sure, that this is a bug in SwiftUI and I will file it. But because Apple barely fixes such bugs, does anyone has an idea for a workaround?
I created my own modifiers to track the scroll position and it works now (although there is sometimes a small lag when scrolling very fast and having the bounce behaviour on for the ScrollView):
struct ContentView: View {
@State private var offset = CGFloat.zero
struct MyStruct: Identifiable {
let id = UUID()
let text: String
let uneven: Bool
var height: CGFloat = 0
}
@State var elements: [MyStruct]
init() {
var el = [MyStruct]()
for i in 1..<30 {
el.append(MyStruct(text: "Item\(i)", uneven: i % 2 == 0))
}
self.elements = el
}
@State var scrollPos: UUID? = nil
var body: some View {
VStack {
if let scrollPos, let el = self.elements.first(where: { $0.id == scrollPos }) {
Text(el.text)
} else {
Text("NaN")
}
ScrollView(.vertical) {
VStack {
ForEach(self.elements) { element in
VStack {
Text(element.text)
LazyVGrid(columns: Array(repeating: GridItem(.fixed(30)), count: 7), content: {
ForEach(0..<7 * (element.uneven == true ? 6 : 7), id: \.self) { index in
Text("\(index)")
}
})
}
.scrollPositionTracking(id: element.id)
}
} // end LazVStack
.frame(width: 600)
}
.scrollPositionTracker(id: self.$scrollPos)
.coordinateSpace(name: "scroll")
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
@Observable
class ScrollTrackerNegatives {
var negatives = [UUID: CGFloat]()
}
struct ScrollPositionTracker: ViewModifier {
@State private var negativesStore = ScrollTrackerNegatives()
var scrollPos: Binding<UUID?>
func body(content: Content) -> some View {
content
.environment(self.negativesStore)
.background(
GeometryReader { proxy in
if self.negativesStore.negatives.count > 0 {
if let max = self.negativesStore.negatives.max(by: { $0.value < $1.value }) {
Task { @MainActor in
self.scrollPos.wrappedValue = max.key
}
}
}
return Color.clear
}
)
}
}
struct ScrollPositionTracking: ViewModifier {
@Environment(ScrollTrackerNegatives.self) private var negativesStore
let id: UUID
func body(content: Content) -> some View {
content
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: $0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { posY in
if posY < 0 {
Task { @MainActor in
self.negativesStore.negatives[id] = posY
}
} else {
Task { @MainActor in
self.negativesStore.negatives.removeValue(forKey: id)
}
}
}
}
}
extension View {
func scrollPositionTracker(id: Binding<UUID?>) -> some View {
self
.modifier(ScrollPositionTracker(scrollPos: id))
}
func scrollPositionTracking(id: UUID) -> some View {
self
.modifier(ScrollPositionTracking(id: id))
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With