Reputation: 30461
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)?
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?
Upvotes: 3
Views: 3800
Reputation: 89
For some strange reason, ScrollView + LazyVStack + onAppear only works as you expect on a real device. Try a device instead of a simulator.
Upvotes: 1
Reputation: 1
In my case the problem was I put my LazyVStack
in ScrollView
, which in turn was in another ScrollView
(easy to lose track of it).
Once that was sorted out everything started to work.
Upvotes: 0
Reputation: 151
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)
}
}
Upvotes: 2
Reputation: 344
It works as per my comments above.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0 ..< 100) { i in
Text("Item: \(i + 1)")
.id(i)
.frame(width: 100, height: 100)
.padding()
.onAppear { print("Appeared:", i + 1) }
}
}
}
}
}
Upvotes: 0
Reputation: 30461
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 onAppear
s 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()
}
}
Upvotes: 1