在 SwiftUI 中,即使是執行簡單的任務時,處理 ScrollView 的步驟都可以變得十分繁瑣;更令人沮喪的是,同一個情況在 UIKit 明明就非常簡單。
不幸的是,其中一個繁瑣的步驟,就是滾動到所需的行列。
在 UIKit 中,每個 UIScrollView
都有一個非常方便的方法,來以編程方式滾動視圖:
func setContentOffset(CGPoint, animated: Bool)
同樣地,每種子類都有相似的方法。例如,我們可以在 UITableView
中使用:
func scrollToRow(at: IndexPath, at: UITableView.ScrollPosition, animated: Bool)
然而,目前 SwiftUI 還沒有這些簡單的功能,因此我們就需要編寫一些比較複雜的程式碼,來執行相同的任務。
在這篇文章中,我們將會一起實作一個這樣的範例 App:
首先,讓我們編寫出簡單的 UI,我們只需要用到 List
和 Picker
視圖。
struct ContentView: View {
@State var selectedIndex: Int = 0
var body: some View {
NavigationView {
VStack {
List {
ForEach(0..<100, id:\.self) { index in
Text("Row number \(index)")
.padding()
}
}
Divider()
Picker("", selection: $selectedIndex) {
ForEach(0..<100, id: \.self) { index in
Text("Go to Row \(index)")
}
}
.pickerStyle(.wheel)
}
.navigationTitle("Scroll To Demo")
}
}
}
當然,這個簡單的 UI 是無法操作的。我們可以試著 wheel picker 選擇一個項目,你會發現它沒有回應。
我們需要一些元件,來觀察及讀取 ScrollView
內發生的事情。
Apple 在 iOS 14 中引入了一個名為 ScrollViewProxy
的 struct
,它的定義是這樣的:
掃描 Proxy 的所有滾動視圖 (scroll view),找出第一個有標識符 (identifier) `id` 的子視圖,然後滾動到該視圖。
在 SwiftUI 官方文檔的同一部分,我們也看到:
我們不會直接創建ScrollViewProxy
的實例。相反,ScrollViewReader
會在其內容視圖構建器中接收ScrollViewProxy
的實例。
因此,我們會按 Apple 所說,使用 ScrollViewReader
視圖!
這個新的元件是一個內建 proxy scrollView 的 View
。讓我們這樣開始修改 UI:
struct ContentView: View {
@State var selectedIndex: Int = 0
var body: some View {
VStack {
NavigationView {
ScrollViewReader { scrollView in
List {
ForEach(0..<100, id:\.self) { index in
Text("Row number \(index)").id(index) // <- NOTE HERE
.padding()
}
}
Divider()
Picker("", selection: $selectedIndex) {
ForEach(0..<100, id: \.self) { index in
Text("Go to Row \(index)")
}
// More to come...
}
.pickerStyle(.wheel)
}
.navigationTitle("Scroll To Demo")
}
}
}
}
從上面的程式碼,我們做了以下的事:
- 在第 8 行,我們使用了
ScrollViewReader
視圖。 - 在第 11 行,我們在
Text
視圖添加了.id(index)
修飾符。在 SwiftUI 中,我們經常會用一個獨特的 id 來標識一個元素;這正是我們在這行程式碼所做的事。
現在,讓我們編寫程式碼,來以編程方式滾動列表吧!我在第 22 行添加了註解 // More to come...
,我們將在這裡綁定 picker 的選擇和操作。
讓我們加入以下程式碼:
.onReceive(Just(selectedIndex)) { value in
scrollView.scrollTo(selectedIndex, anchor: .center)
}
由於 Just
運算符 (operator) 是來自 Combine 框架的,因此我們需要在程式碼開頭匯入這個框架:#import Combine
。
完成了!
現在來當我們選擇一個項目時,上方的 List
就會滾動到指定的行列。
不過你可能也覺得,這段程式碼雖然可以運作,但還是不夠好,那個「跳轉」影響了整個 UX。
讓我們為滾動動作添加一個簡單而好看的動畫:
.onReceive(Just(selectedIndex)) {value in
withAnimation {
scrollView.scrollTo(
selectedIndex, anchor: .center
)
}
}
範例 App 完成了!以下是完整的程式碼:
import SwiftUI
import Combine
struct ContentView: View {
@State var selectedIndex: Int = 0
var body: some View {
VStack {
NavigationView {
ScrollViewReader { scrollView in
List {
ForEach(0..<100, id:\.self) { index in
Text("Row number \(index)").id(index)
.padding()
}
}
Divider()
Picker("", selection: $selectedIndex) {
ForEach(0..<100, id: \.self) { index in
Text("Go to Row \(index)")
}
.onReceive(Just(selectedIndex)) {value in
withAnimation {
scrollView.scrollTo(
selectedIndex, anchor: .center
)
}
}
}
.pickerStyle(.wheel)
}
.navigationTitle("Scroll To Demo")
}
}
}
}
只需要 30 多行程式碼,我們就實作好需要的功能了。
如你所見,明白了操作的機制之後,整個實作過程其實非常簡單,只需要為要滾動到的項目添加一個 id 就可以了。
謝謝你的閱讀。如果你想看看這篇文章的教學影片,可以到我的 YouTube 頻道觀看: