0
votes

I have a ScrollView that will support infinite scroll (it’s a calendar), and so I’m using a LazyVStack for its contents.

I need to know at all times what the first visible item is. To be clear, as long as any of the view is visible — however slight — it should be marked as such. Less concerned with what feels like the first visible but what technically is.

I’ve tried depending on .onAppear and .onDisappear like in this basic example below:

struct RootView: View {
var body: some View {
    ScrollView(.vertical) {
        LazyVStack(spacing: .zero) {
            ForEach(0..<100, id: \.self) { item in
                VStack {
                    Text("\(item)")
                }
                .frame(height: 200)
                .frame(maxWidth: .infinity)
                .background(Color.random)
                .onAppear {
                    print("\(item) appeared")
                }
                .onDisappear {
                    print("\(item) disappeared")
                }
            }
        }
    }.ignoresSafeArea()
    
}

extension Color {
    /// Return a random color
    static var random: Color {
        return Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
}

The issue with this approach is that there appears to be delays in these getting called. It can sometimes be immediate (i.e. when it should), but often it’s with a second or more delay. In a worst-case scenario, it’s never called if I scroll past the item too quickly. This isn’t any help because it’s important the UI updates as soon as the first visible item has changed. From my understanding, this is normal behaviour because there’s no guarantees that these get called as soon as a view becomes visible/hidden.

I’ve considered tracking the offset of each view and then checking with the scrollview’s but have been unable to identify a pattern when comparing numbers that would result in the correct item being identified as the first visible.

This seems like such a basic request but it doesn’t seem to have been answered by anyone (Get first visible index from LazyHStack in SwiftUI is close but I tried it and it’s buggy — depending on the scroll speed, some items’ visibility can be skipped entirely). Preferably the solution — if there is one — doesn’t involve a GeometryReader wrapper because considering the number of items, I fear it will be a major performance drain.

If onAppear is not called then it means SwiftUI rendering engine decides to skip rendering that view, so it will not be visible anyway, and rather next (or next after next) view's onAppear will be called, and you just need to react on that.Asperi
Sure this I could accept but the problem is sometimes (too often even as mentioned in the post) an item that is visible doesn’t have its onAppear called as soon as it becomes visible but with a short yet noticeable delay.Barrrdi
It should be easily reproducible with the source code provided.Barrrdi