0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIでScrollViewのスクロール方向を検知してヘッダーの見た目を変える

0
Posted at

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と違って少しクセがありますが、一度パターン化するとかなり応用が効きます。


0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?