はじめに
こんにちは!
アプリ開発が好きで、Swiftの勉強をしている大学生です。
温かい目で見ていただけると幸いです。
やりたいこと
下記のような特定のViewに対して、スクロールに応じてViewをあげたり下げたりするようにしたいです。
個人開発ではCustomTabBarに対して使ってました。(※Twitterでの評判があまり良くなく、廃止しました)

実装イメージ
- ScrollViewのoffsetを取得する
- 取得したoffsetの値に応じてViewを下げるか上げるかを判断する
- それをView側に通知して、offsetを下げたり上げたりする。
このような感じのイメージをを持ちながら、実装していきます。
実装
まずこちらの記事を参考に、ScrollViewの座標を取得します。
PreferenceKeyの設定
public struct ScrollViewOffsetPreferenceKey: PreferenceKey {
public typealias Value = CGFloat
public static var defaultValue = CGFloat.zero
public static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
ScrollViewをラップして、独自のScrollViewを作ります。
public struct ScrollViewOffset<V: View>: View {
private let coordinateSpaceName = "scrollViewSpaceName"
@State private var oldOffset: CGFloat = 0
@State private var scrollState: ScrollDirection = .up
private let content: V
public init(@ViewBuilder content: () -> V) {
self.content = content()
}
public var body: some View {
ScrollView {
content
.background(
GeometryReader { proxy in
let offset = proxy.frame(in: .named(coordinateSpaceName)).minY
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self, value: offset)
}
)
}
.coordinateSpace(name: coordinateSpaceName)
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
if value >= 0 {
isUpDown(for: .up)
} else if value > oldOffset {
isUpDown(for: .up)
} else {
isUpDown(for: .down)
}
oldOffset = value
}
}
// もっといい命名絶対ある
private func isUpDown(for scrollState: ScrollDirection) {
switch scrollState {
case .up:
NotificationCenter.default.post(name: .up, object: nil)
case .down:
NotificationCenter.default.post(name: .down, object: nil)
}
}
}
今回はNotificationCenter
を使って、onPreferenceChange
で受け取ったvalue
に応じてViewを上げるか下げるかを通知します。
Notificaion
のname
はわかりやすいように下記のようなextension
を定義してあります。
public extension Notification.Name {
static let up = Self("up")
static let down = Self("down")
}
以上で下準備は完了です。
上記の利用方法は以下のとおりです。
public struct HogeView: View {
@State private var isUP = false
public var body: some View {
ScrollViewOffset {
VStack {
Text("これ下げます☟")
Circle()
.foregroundColor(.clear)
.frame(width: 200, height: 200)
.background(HogeBackGround())
.offset(y: isUP ? 0 : 200)
.onReceive(NotificationCenter.default.publisher(for: .up)) { _ in
isUP = true
}
.onReceive(NotificationCenter.default.publisher(for: .down)) { _ in
isUP = false
}
}
}
}
}
自作したScrollViewを使い、onReceive
で検知します。
あとはViewを上げるか下げるかどうかのフラグを変数で保持し、それに応じて直接offsetを好きな値に変更することで実装できました。
今回はViewを下げるだけでしたが、上げることもできますのでぜひ試してみてください。
Tips
onReceive
の引数に記述しているNotification
が少し冗長なので、下記のようなextension
を定義することでみやすくなります
public extension View {
func onReceive(_ name: Notification.Name, perform: @escaping () -> Void) -> some View {
onReceive(NotificationCenter.default.publisher(for: name), perform: { _ in perform() })
}
}
また、この実装にあたってかなり勉強になった記事です🙇
終わりに
誰かの役に立つことができていれば幸いです。
アウトプットを頑張ろうと思っているので温かい目で見ていただけると嬉しいです。