SwiftUIでScrollViewのスクロール方向を検知してヘッダーの見た目を変える
SwiftUIで「スクロールしたらヘッダーをぼかす」や「スクロール方向を検知したい」と思ったことはありませんか?
UIKitだと scrollViewDidScroll で簡単に取れますが、SwiftUIでは少し工夫が必要です。
本記事では、ScrollViewのスクロール量を取得し、スクロール方向を判定する方法をご紹介します。
完成イメージ
- スクロールするとヘッダーが半透明になる
- 上方向スクロールを検知
実装コード
import SwiftUI
struct ContentView: View {
@State private var offsetY = CGFloat.zero
@State private var defaultOffset: CGFloat?
@State var isScrollUp: Bool = false
var body: some View {
ScrollView {
VStack {
ForEach(0...100, id: \.self) { y in
Text("Title: \(y)")
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 60)
.background(.blue)
.padding(.horizontal, 10)
}
}
.onChangeParentScrollViewOffset { value in
if defaultOffset == nil {
defaultOffset = value.rounded
}
isScrollUp = defaultOffset! <= value.rounded - 1
}
}
.safeAreaInset(edge: .top, spacing: 0) {
ZStack {
Text("Scroll up")
.font(.headline)
.frame(maxWidth: .infinity)
.frame(height: 100)
}
.background(
Group {
if isScrollUp {
Rectangle().fill(.ultraThinMaterial)
} else {
Rectangle().fill(Color.clear)
}
}
.ignoresSafeArea(edges: .top)
)
}
}
}
ScrollViewのoffsetを取得する方法
SwiftUIでは直接スクロール位置を取得するAPIがないため、
PreferenceKey と GeometryReader を使います。
extension View {
func onChangeParentScrollViewOffset(perform: @escaping (CGFloat) -> Void) -> some View {
self
.background(GeometryReader {
Color.clear.preference(
key: OffsetPreferenceKey.self,
value: $0.frame(in: .global).origin.y
)
})
.onPreferenceChange(
OffsetPreferenceKey.self,
perform: perform
)
}
}
PreferenceKeyの定義
struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
value = -value
}
}
👉 reduce で値を合計しつつ、符号を反転させています
(スクロール方向を直感的に扱うため)
スクロール方向の判定
if defaultOffset == nil {
defaultOffset = value.rounded
}
isScrollUp = defaultOffset! <= value.rounded - 1
- 初期位置を
defaultOffsetとして保持 - 現在位置との差分でスクロール方向を判定
CGFloatの丸め処理
スクロール値は細かく変動するため、誤差対策として丸めています。
extension CGFloat {
var rounded: CGFloat {
(self * 100).rounded() / 100
}
}
ハマりポイント
❌ round()は使えない
round(self * 100) // 戻り値がVoidなのでNG
👉 必ず rounded() を使う必要があります
応用アイデア
この仕組みを使うと:
- ナビゲーションバーの透明度変更
- スクロールでボタン表示/非表示
- Stickyヘッダー
- 無限スクロール
なども実装可能です。
まとめ
- SwiftUIでスクロール位置を取得するには
PreferenceKeyを使う -
GeometryReaderで座標を取得 - 差分でスクロール方向を判定
UIKitと違って少しクセがありますが、一度パターン化するとかなり応用が効きます。