LazyVStack - 行 onAppear 被提前调用

LazyVStack - row onAppear is called early

我有一个 LazyVStack,有很多行。代码:

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

最初屏幕上只有大约 40 行可见,但 onAppear 触发了 77 行。这是为什么,为什么在屏幕上实际可见之前调用它?我不明白为什么 SwiftUI 必须 'preload' 它们。

有没有办法解决这个问题,或者如果这是有意为之,我如何才能准确知道最后一个 visible 项(接受不同的行高)?

编辑

LazyVStackdocumentation 状态:

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

所以我想这一定是一个错误?

根据documentation的话,onAppear不应该是这样的:

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

但是,如果您在使它正常工作时遇到问题,请参阅下面我的解决方案。


虽然我不确定为什么 onAppear 行会提前触发,但我已经创建了一个变通解决方案。这会读取滚动视图边界的几何形状和要跟踪的单个视图,比较它们,并设置它是否可见。

在此示例中,当最后一项的上边缘在滚动视图的边界中可见时,isVisible 属性 发生变化。由于安全区域的原因,当它在屏幕上可见时,这可能不会出现,但您可以根据需要进行更改。

代码:

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()
    }
}

它按照我上面的评论工作。

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) }
                }
            }
        }
    }
}