はじめに
iOS13のSwiftUIではできなかったコードによるリストのスクロール処理が、iOS14ではできるようになりました。
本記事でその実装方法をまとめています。
iOS13でのスクロール処理
iOSアプリを開発しているとよくある「○番目のセルに自動でスクロールする」という処理ですが、
これをiOS13のSwiftUIでは実現する方法がありませんでした。
struct ContentView: View {
var body: some View {
List(0..<100) {
Text("\($0)")
}
}
}
UIKitでは以下のような形で、scrollToRow
, scrollToItem
などのメソッドを呼び出す形で該当の要件を簡単に実装できるので、
この機能のためだけにUIViewRepresentable
, UIViewControllerRepresentable
などを利用することもしばしばありました。
// UITableView
tableView.scrollToRow(at: IndexPath(row: 10, section: 0),
at: .top,
animated: true)
// UICollectionView
collectionView.scrollToItem(at: .init(item: 10, section: 0),
at: .top,
animated: true)
iOS14でのスクロール処理
iOS14では ScrollViewReader
というスクロール状態を制御できる新しいViewが追加されており、
これを利用すると任意の地点に自動でスクロールさせる処理の実装が可能になります。
以下にサンプルコードを示します。
準備
まずは、準備としてスクロール地点を指定するためのTextFieldを設置します。
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
HStack {
TextField("input row number", text: $text)
Button("Scroll") {
guard let row = Int(text) else { return }
print(row)
}
}.padding()
List(0..<100) {
Text("\($0)")
}
}
}
}
これにより、ボタンをタップするとユーザーがTextFieldに入力した値を取得できるようになります。
スクロール制御
それでは実際にスクロールさせる処理の実装部分です。
まずはスクロール制御をしたい箇所を ScrollViewReader
で囲います。
これによりスクロール制御が可能な ScrollViewProxy
インスタンスの取得ができるようになります。
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
+ ScrollViewReader { (proxy: ScrollViewProxy) in
HStack {
TextField("input row number", text: $text)
Button("Scroll") {
guard let row = Int(text) else { return }
}
}.padding()
List(0..<100) {
Text("\($0)")
}
+ }
}
}
}
さらに、スクロール位置を特定するためのIDを対象のViewに対して付与しておき、
制御を開始したい部分で ScrollViewProxy
の scrollTo
メソッドにそのIDを指定するだけでスクロール処理を実現することができます。
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
ScrollViewReader { (proxy: ScrollViewProxy) in
HStack {
TextField("input row number", text: $text)
Button("Scroll") {
guard let row = Int(text) else { return }
+ withAnimation {
+ proxy.scrollTo(row, anchor: .top)
+ }
}
}.padding()
List(0..<100) {
Text("\($0)")
+ .id($0)
}
}
}
}
}
これでやりたいことが実現できるようになりました。
ちなみに withAnimation
を付与しなければ、スクロールのアニメーションは行われなくなります。
また、第二引数の anchor
でスクロール後の位置を細かく指定できます。
おまけ
Gridに対しても問題なく動作しました。
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
ScrollViewReader { (proxy: ScrollViewProxy) in
HStack {
TextField("input row number", text: $text)
Button("Scroll") {
guard let row = Int(text) else { return }
withAnimation {
proxy.scrollTo(row, anchor: .top)
}
}
}.padding()
ScrollView {
LazyVGrid(
columns: [
GridItem(.flexible(minimum: 0, maximum: .infinity)),
GridItem(.flexible(minimum: 0, maximum: .infinity)),
],
alignment: .center,
spacing: nil
) {
ForEach(0..<100) {
Text("\($0)")
.frame(height: 100)
}
}
}
}
}
}
}