LoginSignup
40

More than 3 years have passed since last update.

iOS14のSwiftUIではリストのスクロール処理をコードで制御できるようになった

Last updated at Posted at 2020-10-14

はじめに

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に対して付与しておき、
制御を開始したい部分で ScrollViewProxyscrollTo メソッドにその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)
                        }
                    }
                }
            }
        }
    }
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40