何曾幾時,在 SwiftUI 開發中的禿頭小碼農們迫切需要一種能夠讀取當前滾動狀態的方法。
在過去,他們往往需要借助于 UIKit 的神秘力量。不過這一切在 SwiftUI 6.0 中已成“滄海桑田”。
在本篇博文中,您將學到如下內容:
- 1. ScrollView 滾動階段簡介
- 2. 普度眾生的 SwiftUI 6.0
- 3. 滾動階段更改上下文(ScrollPhaseChangeContext)
- 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 種滾動階段:
@frozen public enum ScrollPhase : Equatable {case idlecase trackingcase interactingcase deceleratingcase animatingpublic var isScrolling: Bool { get }
}
這些滾動階段的含義如下所示:
- Idle - 表示當前滾動視圖處于空閑狀態,可以認為“嘛事沒有”;
- Tracking - 表示當前用戶正輕觸滾動視圖但并沒有開始滾動;
- Interacting - 表示用戶正在開始或繼續滾動著視圖的內容;
- Decelerating -表示用戶已結束滾動操作,滾動視圖的滾動正在減速直至靜止狀態;
- Animating - 表示滾動視圖被 ScrollPosition 或 ScrollViewReader 類型通過代碼動態滾動到了指定的位置;
另一方面,我們有了新的視圖改器方法 onScrollPhaseChange 專注于滾動階段的監聽:
有了以上兩者的珠聯璧合,現在我們在 SwiftUI 6.0 即可輕而易舉的監聽任何滾動視圖的滾動階段啦:
struct ContentView: View {var body: some View {ScrollView {ForEach(1...50, id: \.self) { i inText("Item \(i)").font(.title).padding()Divider()}}.onScrollPhaseChange { old, new inguard old != new else { return }print("new phase: \(new)")}}
}
在 Xcode 16beta 中運行效果如下所示:
ScrollPhase 類型還提供一個 isScrolling 計算屬性,我們可以用它來判斷當前是否正在滾動。比如,假若視圖正在被滾動我們就“遮擋”它的顯示內容:
struct ContentView: View {@State var isScrolling = falsevar body: some View {ScrollView {ForEach(1...50, id: \.self) { i inText("Item \(i)").font(.title).padding()Divider()}.redacted(reason: isScrolling ? .placeholder : [])}.onScrollPhaseChange { old, new inguard old != new else { return }print("正在滾動?\(new.isScrolling)")isScrolling = new.isScrolling}}
}
執行效果如下圖所示:
3. 滾動階段更改上下文(ScrollPhaseChangeContext)
除此之外,SwiftUI 6.0 中新增的 onScrollPhaseChange 修改器還提供另一種重載(Overloading)形式,在該重載方法的閉包中我們會得到一個 ScrollPhaseChangeContext 上下文對象,使用它我們可以更多的掌控滾動的其它全局信息:
nonisolated
func onScrollPhaseChange(_ action: @escaping (ScrollPhase, ScrollPhase, ScrollPhaseChangeContext) -> Void) -> some View
演示代碼如下所示,可以看到在其中我們使用 ScrollPhaseChangeContext 上下文對象打印出了更多的與滾動相關的信息:
struct ContentView: View { var body: some View {ScrollView {ForEach(1...50, id: \.self) { i inText("Item \(i)").font(.title).padding()Divider()} }.onScrollPhaseChange { old, new, context inguard 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 上將會“徒勞無功”:
struct ContentView: View { var body: some View {List {ForEach(1...50, id: \.self) { i inText("Item \(i)").font(.title).padding()} }.onScrollPhaseChange { old, new, context inguard old != new else { return }print("\(new)\n\(context)")}}
}
上述代碼附著在 List 之上的 onScrollPhaseChange 修改器回調閉包將會無所事事,直接淪為“不舞之鶴”。
誠然我們可以使用 ScrollView 來平替 List,不過如果能在 List 上直接監聽滾動階段豈不更妙?
在 iOS 18.0beta 中,我們可以通過將 List 包裹在 Form 容器中暫時繞開此問題:
struct ContentView: View { var body: some View {Form {List {ForEach(1...50, id: \.self) { i inText("Item \(i)").font(.title).padding()}}}.onScrollPhaseChange { old, new, context inguard old != new else { return }print("\(new)\n\(context)")}}
}
運行代碼可以看到,我們用 onScrollPhaseChange 修改器成功的捕獲到了 List 中滾動階段的改變以及其它滾動信息:
我不確定這一情況在 iOS 18.0 正式版中是否能夠修復,讓我們拭目以待吧!
總結
在本篇博文中,我們介紹了 SwiftUI 6.0(iOS 18.0)滾動視圖最新的滾動階段(Scroll Phase)監聽功能,并討論了如何在原本不支持該功能的列表(List)上使用它。
感謝觀賞,再會啦!😎