📏
SwiftUI でScrollView のスクロール量を取得する
ScrollView のスクロールに連動して見た目が変わるUI を作りたいと思った時のメモです。
困ったこと
UIKit のUIScrollView では、UIScrollViewDelegate のscrollViewDidScroll
を利用することで、スクロールビューのスクロールがどのくらい行われたかを取得することができます。
一方SwiftUI のScrollView には、同じことをやろうとするためのAPI が見つかりませんでした。(ScrollViewReader あたりで出来るのかなと思ったのですが...)
実装前
次のView 要素に、コンテントオフセットを取得する実装を追加していきます。
var body: some View {
ZStack(alignment: .top) {
ScrollView() {
contentA
contentB
contentC
}
header
}
}
コンテントオフセットを取得する
【SwiftUI】 ListでOffsetを取得する - Qiita を参考にさせて頂きました。
GeometryReader
と preference
を使うことによって、要素の位置が変わる度にその位置を取得できます。
@State private var offsetY = CGFloat(0)
struct OffsetYPreferenceKey: PreferenceKey {
typealias Value = [CGFloat]
static var defaultValue: [CGFloat] = [0]
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
var body: some View {
ZStack(alignment: .top) {
ScrollView() {
GeometryReader { geometry in
VStack {
contentA
contentB
contentC
}
.preference(key: OffsetYPreferenceKey.self, value: [geometry.frame(in: .global).minY])
}
}
.onPreferenceChange(OffsetYPreferenceKey.self) { value in
self.offsetY = value[0]
print(self.offsetY)
}
header
}
}
アプリをビルドして実際にスクロールを行うと、次のようなログが出力されます。
47.009440104166686
36.34277343750002
25.342773437500018
17.342773437500018
9.67610677083335
3.0094401041666856
-1.6572265624999787
-6.657226562499979
今回はコードを簡易にするためglobal の座標情報を直接使うようにしました。
参考記事のように座標の差分を取得することで、汎用的に利用できるView になりそうです。
余談
GeometryReader を追加したことでレイアウトが崩れてしまいました。
具体的にはVStack 要素が画面内に収まる高さに固定されるようになってしまいました。 (複数行テキストが1行固定になってしまったのですぐに気がつきました。)
そこで対策として以下を行いました。
- ZStack の中にGeometryReader とVStack 要素を並べる
- GeometryReader の中の要素には
Color.clear
を置く
var body: some View {
ZStack(alignment: .top) {
ScrollView() {
ZStack {
GeometryReader { geometry in
Color.clear
.preference(key: OffsetYPreferenceKey.self, value: [geometry.frame(in: .global).minY])
}
VStack {
contentA
contentB
contentC
}
}
//
取得した値を使う
見た目を変えたい要素の方で、State で宣言したoffset 値を利用できます。
let offsetYThreshold = CGFloat(32)
var header: some View {
ZStack {
//
}
.opacity(offsetYThreshold < offsetY ? 1 : 0)
.animation(.easeInOut)
}
``
Discussion