如何检查项目是否可见 - SwiftUI ScrollView

15

在SwiftUI的ScrollView中,尝试以编程方式确定项目何时显示在屏幕上。我知道ScrollView一次渲染整个视图,而不是像List一样逐个渲染(这也是我必须使用ScrollView的原因之一,因为我有.scrollTo操作)。

我还知道,在UIScrollView中,可以在UIScrollViewDelegate中使用CGRectIntersectsRect来判断项目帧和ScrollView帧之间的交叉情况,但如果可能的话,我更愿意在SwiftUI中找到解决方案。

示例代码如下:

ScrollView {
    ScrollViewReader { action in
        ZStack {
            VStack {
                ForEach(//array of chats) { chat in
                    //chat display bubble
                        .onAppear(perform: {chatsOnScreen.append(chat)})
                }.onReceive(interactionHandler.$activeChat, perform: { _ in
                    //scroll to active chat
                })
            }
        }
    }
}

理想情况下,当用户滚动时,它应该检查屏幕上哪些项目并将视图缩放以适应屏幕上最大的项目。


1
你可以在 ScrollView 中使用 LazyVStack 来触发当项目出现在屏幕上时的 onAppear。但这需要 iOS 14 或以上版本。 - Tomas Jablonskis
2个回答

9

当你在ScrollView中使用VStack时,所有内容都会在构建时一次性创建,因此onAppear不能满足你的意图,相反,你应该使用LazyVStack,它将仅在子视图出现在屏幕上之前创建每个子视图,所以onAppear将在你期望的时候被调用。

因此,代码应该像这样:

ScrollViewReader { action in
   ScrollView {
        LazyVStack {                              // << this !!
            ForEach(//array of chats) { chat in
                //chat display bubble
                    .onAppear(perform: {chatsOnScreen.append(chat)})
            }.onReceive(interactionHandler.$activeChat, perform: { _ in
                //scroll to active chat
            })
        }
    }
}

6
谢谢。这在用户第一次向下滚动时可行,但是当用户向上滚动时,那些聊天记录已经被渲染出来了,缩放不会更新,对吗? - Brad Thomas
1
该函数并不确定视图是否在屏幕上,只是判断操作系统认为它将会出现在屏幕上,因此可以在视图出现在屏幕上之前触发,即使您向上滚动并且视图从未显示。 - Jorge Frias
1
有没有想法只使用真实可见的项来使它工作?甚至有一种方法来检查一个项目有多少是可见的? - iPadawan

1
受到这个答案的启发:https://stackoverflow.com/a/75823183/22499987
如上所述,使用onAppear的问题在于它只保证在每个子视图加载时第一次调用,而不是在滚动回来时调用。
下面的解决方案展示了一个LazyHStack的水平文本元素示例,并使用GeometryReader检测我们所在的索引。我们将每个内部文本元素包装在自己的GeometryReader实例中,用于找到当前的中点x坐标。如果它在屏幕中点的阈值范围内,我们将当前索引设置为此索引。
请注意,此onChange函数将为屏幕上可见的所有文本调用。例如,当我们从1滑动到2时,onChange将跟踪1的x坐标,该坐标大约在屏幕中点附近。当2变为可见时,2的x坐标实际上将是一个负值,因为它的中点位于0的左侧(屏幕的左边缘)。
struct SampleView: View {
    @State private var currIndex: Int = 1

    var body: some View {
        let screenWidth = UIScreen.main.bounds.width
        let numLoops = 10
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack {
                ForEach(1 ..< numLoops + 1, id: \.self) { index in
                    GeometryReader { inner in
                        Text("\(index)")
                        .onChange(of: inner.frame(in: .global).midX) {
                            // x will be the midPoint of the current view
                            let x = inner.frame(in: .global).midX
                            let screenMid = screenWidth / 2
                            let threshold: CGFloat = screenWidth / 10
                            // check if the midpoint of the current view is within our range of the screen mid
                            if x > screenMid - threshold && x < screenMid + threshold {
                                print("\(index) is in the middle")
                                currIndex = index
                            }
                        }
                    }
                    .frame(width: screenWidth, height: 150)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .frame(height: 150)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接