SwiftUIでスクロール位置を制御する方法が、iOS 17で大きく進化しました。従来のScrollViewReaderに代わって登場したscrollPosition(id:)
は、より宣言的で直感的なスクロール制御を可能にします。
この記事では、scrollPosition(id:)
の基本概念から実践的な活用法、ScrollViewReaderとの違い、そして実際のアプリ開発で役立つテクニックまで、包括的に解説します。
scrollPosition(id:)とは
scrollPosition(id:)
は、iOS 17で導入されたSwiftUIの新しいスクロール制御修飾子です。
ScrollViewやListの現在表示されている要素のIDを追跡し、プログラム的にスクロール位置を制御できます。
1 2 |
.scrollPosition(id: $selectedID) |
従来のScrollViewReaderと比較して、以下の特徴があります:
- 双方向バインディング: スクロール位置の取得と設定を同じ変数で管理
- 宣言的な記述: より簡潔で読みやすいコード
- 自動的な状態同期: ユーザーのスクロール操作も自動で状態に反映
たとえば「前回表示していた項目に戻したい」「指定のIDまでジャンプさせたい」などの要件に対して、簡潔に対応できます。
ScrollViewReaderとの比較
ScrollViewReaderの制限
ScrollViewReader
は iOS 14 から使えるため互換性が広い一方で、仕組みがやや複雑でした。
-
スクロール位置の取得ができない
proxy.scrollTo(...)
で「移動」はできても「現在どこにいるか」は取れません。履歴保存や状態同期が難しいです。 -
クロージャ構文が複雑
ScrollViewReader { proxy in ... }
の内側でしか使えず、コードのネストが深くなります。 -
状態管理が手動
どの位置にスクロールしたかを保持するには、自前でStateを管理して更新処理を書く必要がありました。
scrollPosition(id:) の改善点
scrollPosition(id:)
は iOS 17 以降専用ですが、コード量が少なく直感的に書ける のが最大の強みです。
-
修飾子ひとつで完結
.scrollPosition(id:)
をScrollView
やList
に付けるだけでOK。外側のクロージャは不要です。 -
双方向バインディング
@State
にバインドできるため、ユーザーがスクロールした位置を取得でき、プログラムから設定することもできます。 -
アニメーションが自然
withAnimation { selectedID = ... }
でアニメーション付きジャンプが簡単に書けます。ScrollViewReader
ではproxy.scrollTo
にアニメーションを渡す必要がありました。 -
状態が自動同期
スクロール位置とStateが自動で結びつくため、「前回見ていた場所に戻す」などがシンプルに実装可能です。
主な違いの比較表
項目 | ScrollViewReader | scrollPosition(id:) |
---|---|---|
iOS バージョン | iOS 14+ | iOS 17+ |
記述の簡潔性 | 複雑なクロージャ | シンプルな修飾子 |
双方向バインディング | 設定のみ | 取得・設定両対応 |
アニメーション制御 | proxy.scrollTo内で指定 | withAnimationで制御 |
状態の自動同期 | 手動実装が必要 | 自動同期 |
基本的な使用方法
scrollPosition(id:)
は、リストやスクロールビューの中で「どの要素を表示位置の基準にするか」を制御できる強力な修飾子です。
ここでは、最もシンプルな例を通じて使い方を理解していきましょう。
1. 基本的なセットアップ
以下の例では、色付きの矩形をスクロールビューに並べ、メニューから色を選択するとその矩形まで自動スクロールします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
struct BasicScrollPositionView: View { @State private var scrollPosition: String? let colors = ["red", "blue", "green", "yellow", "purple"] func color(from name: String) -> Color { switch name { case "red": return .red case "blue": return .blue case "green": return .green case "yellow": return .yellow case "purple": return .purple default: return .gray } } var body: some View { NavigationView { ScrollView { LazyVStack(spacing: 10) { ForEach(colors, id: \.self) { colorName in Rectangle() .fill(color(from: colorName)) .frame(height: 200) .overlay(Text(colorName.capitalized)) } } .scrollTargetLayout() } .scrollPosition(id: $scrollPosition) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu("移動") { ForEach(colors, id: \.self) { color in Button(color.capitalized) { scrollPosition = color } } } } } } } } |
Menu
内で色を選択すると scrollPosition
が更新され、該当する色の矩形までスムーズにスクロールします。
リストやスクロールビューに scrollPosition(id:)
を追加するだけで、このような直感的な動作を簡単に実現できます。
滑らかにスクロールさせるにはwithAnimationを組み合わせる。
指定した位置まで滑らかにスクロールさせたい場合はwithAnimationを組み合わせましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import SwiftUI struct ContentView: View { @State private var selectedID: Int? var body: some View { NavigationStack { ScrollView { LazyVStack(alignment: .leading) { ForEach(0..<100, id: \.self) { index in Text("Item \(index)") .frame(maxWidth: .infinity, alignment: .leading) .padding() .background(Color(.systemGray6)) .clipShape(RoundedRectangle(cornerRadius: 8)) } } .scrollTargetLayout() } .scrollPosition(id: $selectedID, anchor: .center) .toolbar { Button("50番へ移動") { withAnimation(.easeInOut(duration: 0.5)) { selectedID = 50 } } } .padding() } } } #Preview { ContentView() } |
上記コードでは、50番に移動ボタンを押すと、50番まで滑らかにスクロールします。
2. アンカー位置の指定
さらに scrollPosition(id:anchor:)
を使うことで、スクロール先の要素をどの位置に表示するかを細かく指定できます。
1 2 3 4 |
.scrollPosition(id: $selectedID, anchor: .top) // 上端に配置 .scrollPosition(id: $selectedID, anchor: .center) // 中央に配置 .scrollPosition(id: $selectedID, anchor: .bottom) // 下端に配置</code><code class="language-swift"> |
3.指定する場所
scrollPosition(id:)
を記述する場所にも注意が必要です。
ScrollView
や List
の閉じカッコの直後に .scrollPosition(id:)
を適用するのが基本です。
もし scrollPosition(id:)
を中の LazyVStack
などに誤って付与してしまうと、正しく動作しないので注意が必要です。
scrollPosition(id:) の活用シーン
scrollPosition(id:)
は、ユーザーがスクロール中に見ていた位置や、特定の項目へジャンプする動きをプログラムで制御したいときに非常に役立ちます。
特に長いリストやチャットのようなUIでは「前回の位置を覚えておく」「すぐに最新に移動する」といった体験が求められるため、この修飾子の価値が大きく発揮されます。
- 長いリストの中で「前回開いていた位置に戻す」機能の実装
- タップやボタン操作で特定の項目にジャンプするインターフェース
- データ更新後に該当する行にスクロールしてユーザーに注目させたいとき
- チャットアプリの「最新メッセージへスクロール」などの導線
- タブ切り替え時にスクロール位置を記憶・復元したいとき
このように、表示中の位置を保持・操作したいすべての場面で有効に使えます。
スクロール体験を制御できることで、ユーザーが迷わず操作を続けられるインターフェースを構築できます。
scrollPosition(id:) を使うときの注意点
便利な一方で、scrollPosition(id:)
を使う際にはいくつかの前提条件や制約があります。
これを理解していないと「動かない」「意図しない位置に飛んでしまう」といった問題が起きやすいため、実装時に注意が必要です。
- 利用には iOS 17以降 が必要です(旧バージョンではコンパイルエラーになります)
.id(_:)
を各要素に指定しないと動作しません。必ず識別子を明示しましょうscrollPosition(id:)
は リストやScrollViewにのみ適用可能。他のViewでは無効です- 大量のアイテムを扱う場合、IDの指定を一意に保つことが重要です(衝突すると不正な挙動になる可能性があります)
このような制約を把握しておけば、scrollPosition(id:)
を安定して利用でき、複雑なスクロール制御も安全に実現できます。
まとめ
今回は scrollPosition(id:)
について詳しく紹介しました。
scrollPosition(id:)
は、リストやScrollViewのスクロール位置をIDで制御するための修飾子- IDにバインドすることで、特定の要素へジャンプしたり、現在の位置を記録したりできる
.id(_:)
を組み合わせて使うことが前提- タップでスクロール、表示位置の復元、リスト内ナビゲーションなどに広く使える
- iOS 17以降で利用可能な新しいAPI
スクロールの位置を簡単に追跡・制御できる scrollPosition(id:)
は、ユーザー体験を大きく向上させるための強力なツールです。
複雑なスクロール制御も宣言的に書けるようになるため、ぜひ積極的に取り入れていきましょう。