Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LazyVStack - row onAppear is called early

I have a LazyVStack, with lots of rows. Code:

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(0 ..< 100) { i in
                    Text("Item: \(i + 1)")
                        .onAppear {
                            print("Appeared:", i + 1)
                        }
                }
            }
        }
    }
}

Only about 40 rows are visible on the screen initially, yet onAppear is triggered for 77 rows. Why is this, why is it called before it is actually visible on the screen? I don't see why SwiftUI would have to 'preload' them.

Is there a way to fix this, or if this is intended, how can I accurately know the last visible item (accepting varying row heights)?

Edit

The documentation for LazyVStack states:

The stack is “lazy,” in that the stack view doesn’t create items until it needs to render them onscreen.

So this must be a bug then, I presume?

like image 314
George Avatar asked Dec 10 '25 07:12

George


2 Answers

It seems incredible but just adding a GeometryReader containing your ScrollView would resolve the issue

GeometryReader { _ in
        ScrollView(.vertical, showsIndicators: false) {
            LazyVStack(spacing: 14) {
                Text("Items")
                LazyVStack(spacing: 16) {
                    ForEach(viewModel.data, id: \.id) { data in
                        MediaRowView(data: data)
                        .onAppear {
                            print(data.title, "item appeared")
                        }
                    }
                    if viewModel.state == .loading {
                        ProgressView()
                    }
                }
            }
            .padding(.horizontal, 16)
        }
    }
like image 87
Luchi Parejo Alcazar Avatar answered Dec 12 '25 06:12

Luchi Parejo Alcazar


By words from the documentation, onAppear shouldn't be like this:

The stack is “lazy,” in that the stack view doesn’t create items until it needs to render them onscreen.

However, if you are having problems getting this to work properly, see my solution below.


Although I am unsure why the rows onAppears are triggered early, I have created a workaround solution. This reads the geometry of the scroll view bounds and the individual view to track, compares them, and sets whether it is visible or not.

In this example, the isVisible property changes when the top edge of the last item is visible in the scroll view's bounds. This may not be when it is visible on screen, due to safe area, but you can change this to your needs.

Code:

struct ContentView: View {
    @State private var isVisible = false

    var body: some View {
        GeometryReader { geo in
            ScrollView {
                LazyVStack {
                    ForEach(0 ..< 100) { i in
                        Text("Item: \(i + 1)")
                            .background(tracker(index: i))
                    }
                }
            }
            .onPreferenceChange(TrackerKey.self) { edge in
                let isVisible = edge < geo.frame(in: .global).maxY

                if isVisible != self.isVisible {
                    self.isVisible = isVisible
                    print("Now visible:", isVisible ? "yes" : "no")
                }
            }
        }
    }

    @ViewBuilder private func tracker(index: Int) -> some View {
        if index == 99 {
            GeometryReader { geo in
                Color.clear.preference(
                    key: TrackerKey.self,
                    value: geo.frame(in: .global).minY
                )
            }
        }
    }
}
struct TrackerKey: PreferenceKey {
    static let defaultValue: CGFloat = .greatestFiniteMagnitude

    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue()
    }
}
like image 27
George Avatar answered Dec 12 '25 07:12

George



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!