I am trying to ascertain which GridItem
would effectively be at position x:0, y:0.
To achieve this I am simply trying to use a preferenceKey
and GeometryReader
.
I am adding an .overlay
to my GridItems and on the GridItem
at gridItemIndex 0 adding the GeometryReader
around a Color.clear
. My expected logic is to track the Y position of that GridItem
. Then by dividing that Y offset by the height of each GridItem
I will get which item is currently at the top.
I have this working to a point. Once the GridItem at index "gridItemIndex" position 0 is above a certain offset it is no longer read and the y position rests to 0.0. My assumption for this is due to the view being reused?
Currently I am not getting the reading above 40 but I need to get until the bottom of the LazyVGrid appears.
Here is my code
struct DetectScrollPosition: View {
let gridRowLayout = Array(repeating: GridItem(spacing: 0), count: 7)
@State private var scrollPosition: Int = 0
var body: some View {
NavigationView {
ScrollView (.vertical){
LazyVGrid(columns: gridRowLayout, spacing: 0){
ForEach(0..<1092, id: \.self) { gridItemIndex in
Text("\(abs(gridItemIndex / 7))")
.overlay {
if gridItemIndex == 0 {
GeometryReader { geometryProxy in
Color.clear
.updateViewsYPosition(geometryProxy)
}
}
}
}
}
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.scrollPosition = abs(Int(value))
}
}
.coordinateSpace(.named("scroll"))
.navigationTitle("The Top Row is: \(scrollPosition)")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
}
}
extension View {
func updateViewsYPosition(_ geometryProxy: GeometryProxy) -> some View {
let offset = geometryProxy.frame(in: .named("scroll")).origin.y / geometryProxy.frame(in: .named("scroll")).height
return self.preference(key: ScrollOffsetPreferenceKey.self, value: offset)
}
}
Indeed, the views are being reused. You should add a view that sets the preference for every row of the grid.
.overlay {
if gridItemIndex % 7 == 0 { // adds this for the first view in every row
GeometryReader { geometryProxy in
Color.clear
.updateViewsYPosition(geometryProxy, gridItemIndex)
}
}
}
Now we need to implement the reduce
method in the preference key, because there will be multiple sibling views all setting their own preference. The idea is, after reducing everything, the end result will indicate the view that is on the top left.
Therefore, we need to store both the frame of the view (for reducing) and the index of the view (so that we can update scrollPosition
). We will use a type like this for the preference key:
struct GridItemPosition: Equatable {
let index: Int
let frame: CGRect
}
This is why we also pass in gridItemIndex
to updateViewsYPosition
in the overlay
.
The actual preference key would be implemented like this:
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue = GridItemPosition(index: -1, frame: .null)
static func reduce(value: inout GridItemPosition, nextValue: () -> GridItemPosition) {
let next = nextValue()
if abs(value.frame.minY) > abs(next.frame.minY) {
value = next
}
}
}
reduce
is implemented so that the preference is always the view with a y position that is closest to 0. You can change this criteria to whatever you want.
In updateViewsYPosition
, you should get the frame in the scrollView
coordinate space.
func updateViewsYPosition(_ geometryProxy: GeometryProxy, _ i: Int) -> some View {
let frame = geometryProxy.frame(in: .scrollView)
return self.preference(
key: ScrollOffsetPreferenceKey.self,
value: GridItemPosition(index: i, frame: frame)
)
}
Full code:
struct ContentView: View {
let gridRowLayout = Array(repeating: GridItem(spacing: 0), count: 7)
@State private var scrollPosition: Int = 0
var body: some View {
NavigationStack {
ScrollView (.vertical){
LazyVGrid(columns: gridRowLayout, spacing: 0){
ForEach(0..<1092, id: \.self) { gridItemIndex in
Text("\(abs(gridItemIndex))")
.overlay {
if gridItemIndex % 7 == 0 {
GeometryReader { geometryProxy in
Color.clear
.updateViewsYPosition(geometryProxy, gridItemIndex)
}
}
}
}
}
.scrollTargetLayout()
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.scrollPosition = value.index
}
}
.navigationTitle("The Top Row is: \(scrollPosition)")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue = GridItemPosition(index: -1, frame: .null)
static func reduce(value: inout GridItemPosition, nextValue: () -> GridItemPosition) {
let next = nextValue()
if abs(value.frame.minY) > abs(next.frame.minY) {
value = next
}
}
}
extension View {
func updateViewsYPosition(_ geometryProxy: GeometryProxy, _ i: Int) -> some View {
let frame = geometryProxy.frame(in: .scrollView)
return self.preference(
key: ScrollOffsetPreferenceKey.self,
value: GridItemPosition(index: i, frame: frame)
)
}
}
struct GridItemPosition: Equatable {
let index: Int
let frame: CGRect
}