SwiftUI 6.0(iOS 18)监听滚动视图视口中子视图可见性的极简方法

109 阅读4分钟

在这里插入图片描述

概览

在 SwiftUI 的应用开发中,我们有时需要监听滚动视图中子视图当前的显示状态:它们现在是被滚动到可见视口(Viewport)?或仍然是隐藏在“未知的黑暗”中呢?

在这里插入图片描述

在 SwiftUI 早期版本中为了得偿所愿,我们需要借助一些“取巧”的手段。不过,从 SwiftUI 6.0(iOS 18)开始情况有了改观,我们有了针对性的解决方案。

在本篇博文中,您将学到如下内容:

  1. 监听滚动目标可见性的改变
  2. 监听任意视图滚动可见性的改变
  3. 更改滚动可见性阙值

相信学完本课后,小伙伴们对于 SwiftUI 滚动视图视口可见性的监听一定会有更加浑然天成的解决之道。

那还等什么呢?让我们马上开始滚动大冒险吧!

Let‘s go!!!;)


1. 监听滚动目标可见性的改变

在以滚动目标(Scroll Target)为驱动导向的滚动视图中,我们有多种方法来监听视口中子视图可见性的“白云苍狗”。


想要了解更多 SwiftUI 滚动目标行为的知识,请小伙伴们移步如下链接观赏精彩的内容:


不过,从 SwiftUI 6.0(iOS 18)开始一切都变的更加信手拈来了。在 WWDC 24 中苹果新增了 onScrollTargetVisibilityChange 修改器方法来专注于解决此事:

在这里插入图片描述

使用该修改器,我们可以监听滚动视图视口所有子视图可见性的实时变化情况:

struct ItemsView: View {
    @State private var visible: [Int] = []
    
    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading) {
                ForEach(1..<100, id: \.self) { item in
                    Text("Item \(item)")
                        .font(.largeTitle)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background {
                            Rectangle()
                                .foregroundStyle(.blue.gradient)
                        }
                        .padding()
                        
                }
            }
            .scrollTargetLayout()
        }
        .onScrollTargetVisibilityChange(idType: Int.self) { identifiers in
            visible = identifiers
        }
        .safeAreaInset(edge: .top) {
            Text("当前可见 Item:\(visible)")
                .font(.title)
                .padding()
                .frame(maxWidth: .infinity)
                .background {
                    RoundedRectangle(cornerRadius: 15.0)
                        .foregroundStyle(.thinMaterial)
                }
                .padding()
        }
        .navigationTitle("所有 Items")
    }
}

如上代码所示:我们在 ScrollView 上调用了 onScrollTargetVisibilityChange 方法,实时监听着所有滚动目标的可见性变化。当滚动视图视口内子视图可见性发生改变时,这些改变视图对应的 ID 就会被发送给回调闭包。

运行代码可以看到,当这些 Item 对应 Cell 视图的可见性发生变化时我们能够立即得到反馈:

在这里插入图片描述

2. 监听任意视图滚动可见性的改变

除了滚动视图中多个滚动目标可见性的监听以外,还有另外一种情况:即我们希望监听滚动视图中某个子视图在滚动中的可见性。

SwiftUI 6.0 同样对此做了“体贴入微”的特殊“照顾”,这是通过 onScrollVisibilityChange 修改器方法来实现的:

在这里插入图片描述

使用 onScrollVisibilityChange 修改器方法,我们可以在指定视图“进入或离开屏幕(on/off screen)”时得到通知。

例如,我们可以利用此特性优化播放器的性能:即在播放器加入渲染树时播放视频,而在播放器从渲染树删除时停止播放视频。

struct VideoPlayerView: View {
    let url: URL
    
    @State var player: AVPlayer?
    
    var body: some View {
        VideoPlayer(player: player)
            .task {
                if player == nil {
                    player = AVPlayer(url: url)
                }
            }
            .onScrollVisibilityChange { isVisible in
                if isVisible {
                    player?.play()
                } else {
                    player?.pause()
                }
            }
    }
}

或者对于普通视图来说,我们只是简单的希望它们在“进入和离开”屏幕时能够及时的“告知”我们:

struct DetailView: View {
    
    @State var isCardDisplaying = false
    
    var body: some View {
        Form {
            LabeledContent("姓名", value: "大熊猫侯佩")
            
            LabeledContent("超能力", value: "隐身")
            
            List(1...10, id: \.self) {i in
                Text("助手 \(i)")
                    .font(.title)
                    .padding()
                    .foregroundStyle(.red)
            }
            
            CardView()
                .onAppear {
                    print("card appear")
                }
                .onDisappear {
                    print("card disappear")
                }
                .onScrollVisibilityChange { new in
                    isCardDisplaying = new
                }
            
            List(1...10, id: \.self) {i in
                Text("隐藏技能 \(i)")
                    .font(.title)
                    .padding()
                    .foregroundStyle(.blue)
            }
        }
        .navigationTitle("细节展示")
        .toolbar {
            if isCardDisplaying {
                Text("卡片呈现中...")
                    .font(.title3.weight(.heavy))
                    .foregroundStyle(.blue)
            }
        }
    }
}

不过目前(iOS 18 beta3)来说,在某些场景中应用 onScrollVisibilityChange 修改器会有些许小问题:比如在 Form 中,当子视图离开滚动视口时通知的不及时;或者在 ScrollView 中会有死循环发生。

不知在 iOS 18 正式版中 onScrollVisibilityChange 修改器的体验是否会有所改善,让我们拭目以待吧。

3. 更改滚动可见性阙值

细心的小伙伴们可能都发现了,上面 onScrollTargetVisibilityChange 和 onScrollVisibilityChange 两个修改器都附带一个 threshold 参数,它的默认值为 0.5:

nonisolated
func onScrollTargetVisibilityChange<ID>(
    idType: ID.Type,
    threshold: Double = 0.5,
    _ action: @escaping ([ID]) -> Void
) -> some View where ID : Hashable

nonisolated
func onScrollVisibilityChange(
    threshold: Double = 0.5,
    _ action: @escaping (Bool) -> Void
) -> some View

使用 threshold 实参我们可以调整视图可见性监听的“灵敏度”:

threshold The amount required to be visible within the viewport of the the parent view before the action is fired. By default when the view has crossed more than 50% on-screen, the action will be called.

假如我们希望视图整体全部显露或隐藏时才触发监听回调,可以将这个值设置为 1.0:

CardView()
    .onScrollVisibilityChange(threshold: 1.0) { new in
        print("visibility change to \(new)")
        isCardDisplaying = new
    }

现在,借助于 SwiftUI 6.0 推出的这两个新修改器我们对于滚动视图视口可见性的监听更加得心应手、挥洒自如了!棒棒哒!💯

总结

在本篇博文中,我们讨论了 SwiftUI 6.0(iOS 18)中监听滚动视图视口(Viewport)中子视图滚动可见性的新方法,并给出示例代码。

感谢观赏,再会啦!8-)