SwiftUI 6.0(iOS 18.0)滚动视图新增的滚动阶段(Scroll Phase)监听功能趣谈

何曾几时,在 SwiftUI 开发中的秃头小码农们迫切需要一种能够读取当前滚动状态的方法。

在过去,他们往往需要借助于 UIKit 的神秘力量。不过这一切在 SwiftUI 6.0 中已成"沧海桑田"。

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

  • [1. ScrollView 滚动阶段简介](#1. ScrollView 滚动阶段简介)
  • [2. 普度众生的 SwiftUI 6.0](#2. 普度众生的 SwiftUI 6.0)
  • [3. 滚动阶段更改上下文(ScrollPhaseChangeContext)](#3. 滚动阶段更改上下文(ScrollPhaseChangeContext))
  • [4. 如何监听列表(List)的滚动阶段](#4. 如何监听列表(List)的滚动阶段)
  • 总结

相信学完本课后,小伙伴们在需要监听滚动视图滚动阶段的应用场景中定能得心应手、游刃有余!

那还等什么呢?让我们马上开始吧!Let's go!!!😃


1. ScrollView 滚动阶段简介

所谓滚动阶段(Scroll Phase)是指滚动视图在滚动前、滚动中以及滚动后所处的不同阶段。

早在 macOS 10.9+ 的 CoreGraphics 中就有滚动阶段的概念了:


如果我们能及时的读取各个滚动阶段的值,我们就可以根据它们为滚动视图提供更加"银杏化"的定制和更流畅滚动附加体验。

在 SwiftUI 6.0 之前,我们无法使用行之有效的方法来读取滚动视图当前所处的滚动阶段,只有委身救助于 UIKit 的秉轴持钧。

然而,这一切在 SwiftUI 6.0 中有了翻天覆地的变化!

2. 普度众生的 SwiftUI 6.0

自从 SwiftUI 6.0(iOS 18.0)开始,"顿悟"的苹果终于提供滚动阶段的监听功能了。

一方面,我们有了描述滚动阶段的新类型 ScrollPhase:

它包含 5 个枚举值分别对应于 5 种滚动阶段:

swift 复制代码
@frozen public enum ScrollPhase : Equatable {
    case idle
    case tracking
    case interacting
    case decelerating
    case animating
    
    public var isScrolling: Bool { get }
}

这些滚动阶段的含义如下所示:

  • Idle - 表示当前滚动视图处于空闲状态,可以认为"嘛事没有";
  • Tracking - 表示当前用户正轻触滚动视图但并没有开始滚动;
  • Interacting - 表示用户正在开始或继续滚动着视图的内容;
  • Decelerating -表示用户已结束滚动操作,滚动视图的滚动正在减速直至静止状态;
  • Animating - 表示滚动视图被 ScrollPosition 或 ScrollViewReader 类型通过代码动态滚动到了指定的位置;

另一方面,我们有了新的视图改器方法 onScrollPhaseChange 专注于滚动阶段的监听:

有了以上两者的珠联璧合,现在我们在 SwiftUI 6.0 即可轻而易举的监听任何滚动视图的滚动阶段啦:

swift 复制代码
struct ContentView: View {
        
    var body: some View {
        ScrollView {
            
            ForEach(1...50, id: \.self) { i in
                Text("Item \(i)")
                    .font(.title)
                    .padding()
                
                Divider()
            }
        
        }
        .onScrollPhaseChange { old, new in
            guard old != new else { return }
            print("new phase: \(new)")
            
        }
    }
}

在 Xcode 16beta 中运行效果如下所示:

ScrollPhase 类型还提供一个 isScrolling 计算属性,我们可以用它来判断当前是否正在滚动。比如,假若视图正在被滚动我们就"遮挡"它的显示内容:

swift 复制代码
struct ContentView: View {
    
    @State var isScrolling = false
        
    var body: some View {
        ScrollView {
            
            ForEach(1...50, id: \.self) { i in
                Text("Item \(i)")
                    .font(.title)
                    .padding()
                
                Divider()
            }
            .redacted(reason: isScrolling ? .placeholder : [])
        
        }
        .onScrollPhaseChange { old, new in
            guard old != new else { return }
            print("正在滚动?\(new.isScrolling)")
            isScrolling = new.isScrolling
        }
    }
}

执行效果如下图所示:

3. 滚动阶段更改上下文(ScrollPhaseChangeContext)

除此之外,SwiftUI 6.0 中新增的 onScrollPhaseChange 修改器还提供另一种重载(Overloading)形式,在该重载方法的闭包中我们会得到一个 ScrollPhaseChangeContext 上下文对象,使用它我们可以更多的掌控滚动的其它全局信息:

swift 复制代码
nonisolated
func onScrollPhaseChange(_ action: @escaping (ScrollPhase, ScrollPhase, ScrollPhaseChangeContext) -> Void) -> some View

演示代码如下所示,可以看到在其中我们使用 ScrollPhaseChangeContext 上下文对象打印出了更多的与滚动相关的信息:

swift 复制代码
struct ContentView: View {        
    var body: some View {
        ScrollView {
            
            ForEach(1...50, id: \.self) { i in
                Text("Item \(i)")
                    .font(.title)
                    .padding()
                
                Divider()
            }        
        }
        .onScrollPhaseChange { old, new, context in
            guard old != new else { return }
            print("\(new)\n\(context)")            
        }
    }
}

运行结果如下所示:

ScrollPhaseChangeContext(geometry: <ScrollGeometry: contentOffset (0.0, 1694.3333333333333), contentSize (393.0, 4092.0), contentInsets <top: 59.0, leading: 0.0, bottom: 34.0, trailing: 0.0>, containerSize (393.0, 759.0), visibleRect (0.0, 1694.3333333333333, 393.0, 852.0)>, velocity: Optional((0.0, 0.0)))

4. 如何监听列表(List)的滚动阶段

虽然 SwiftUI 6.0 破茧而出的"大杀器" onScrollPhaseChange 对于我们监听滚动状态大有裨益,不过目前它只能应用在 ScrollView 视图的外层。这意味着,如果将其放在 List 上将会"徒劳无功":

swift 复制代码
struct ContentView: View {        
    var body: some View {
        List {
            ForEach(1...50, id: \.self) { i in
                Text("Item \(i)")
                    .font(.title)
                    .padding()
            }        
        }
        .onScrollPhaseChange { old, new, context in
            guard old != new else { return }
            print("\(new)\n\(context)")
        }
    }
}

上述代码附着在 List 之上的 onScrollPhaseChange 修改器回调闭包将会无所事事,直接沦为"不舞之鹤"。

诚然我们可以使用 ScrollView 来平替 List,不过如果能在 List 上直接监听滚动阶段岂不更妙?

在 iOS 18.0beta 中,我们可以通过将 List 包裹在 Form 容器中暂时绕开此问题:

swift 复制代码
struct ContentView: View {        
    var body: some View {
        Form {
            List {
                ForEach(1...50, id: \.self) { i in
                    Text("Item \(i)")
                        .font(.title)
                        .padding()
                }
            }
        }
        .onScrollPhaseChange { old, new, context in
            guard old != new else { return }
            print("\(new)\n\(context)")
        }
    }
}

运行代码可以看到,我们用 onScrollPhaseChange 修改器成功的捕获到了 List 中滚动阶段的改变以及其它滚动信息:

我不确定这一情况在 iOS 18.0 正式版中是否能够修复,让我们拭目以待吧!

总结

在本篇博文中,我们介绍了 SwiftUI 6.0(iOS 18.0)滚动视图最新的滚动阶段(Scroll Phase)监听功能,并讨论了如何在原本不支持该功能的列表(List)上使用它。

感谢观赏,再会啦!😎

相关推荐
大熊猫侯佩13 天前
WWDC24(Xcode 16)中全新的 Swift Testing 使用进阶
单元测试·xctest·xcode 16·wwdc 24·swift testing·初始化和清理·测试顺序
大熊猫侯佩13 天前
用接地气的例子趣谈 WWDC 24 全新的 Swift Testing 入门(三)
单元测试·xcode 16·wwdc 24·swift 宏·swift testing·#expect·#require
大熊猫侯佩1 个月前
SwiftUI 6.0(iOS 18)新增的网格渐变色 MeshGradient 解惑
动画·颜色·ios 18·swiftui 6.0·渐变色·gradient·网格渐变色
大熊猫侯佩1 个月前
SwiftUI 6.0(iOS 18)自定义容器值(Container Values)让容器布局渐入佳境(上)
foreach·group·layout·ios 18·swiftui 6.0·containervalues·自定义容器
Se7en丶潇洒哥2 个月前
Xcode 16 上传AppStore遇到第三方库 bitcode 的问题
ios·xcode·appstore·xcode 16·bitcode
大熊猫侯佩4 个月前
SwiftUI 5.0(iOS 17)滚动视图的滚动目标行为(Target Behavior)解惑和实战
滚动·scrollview·swiftui 5.0·ios 17·target behavior·惰性容器·滚动对齐
大熊猫侯佩4 个月前
SwiftUI 6.0(Xcode 16)新 PreviewModifier 协议让预览调试如虎添翼
ios 18·xcode 16·swiftui 6.0·previewmodifier·预览调试·inject 注入测试数据·mock data
大熊猫侯佩4 个月前
迂回战术:“另类“全新安装 macOS 15 Sequoia beta2 的极简方法
macos·磁盘空间·sequoia·xcode 16·macos 15·全新安装·beta
大熊猫侯佩5 个月前
SwiftUI 6.0(iOS 18/macOS 15)关于颜色 Color 的新玩法
color·ios 18·xcode 16·swiftui 6.0·macos 15·混合颜色·渐变动画