1
0

More than 1 year has passed since last update.

[SwiftUI] スクロールに応じて特定のViewを下げたり上げたりしたい

Posted at

はじめに

こんにちは!
アプリ開発が好きで、Swiftの勉強をしている大学生です。
温かい目で見ていただけると幸いです。

やりたいこと

下記のような特定のViewに対して、スクロールに応じてViewをあげたり下げたりするようにしたいです。
個人開発ではCustomTabBarに対して使ってました。(※Twitterでの評判があまり良くなく、廃止しました)

実装イメージ

  1. ScrollViewのoffsetを取得する
  2. 取得したoffsetの値に応じてViewを下げるか上げるかを判断する
  3. それを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を上げるか下げるかを通知します。
Notificaionnameはわかりやすいように下記のような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() })
    }
}

また、この実装にあたってかなり勉強になった記事です🙇

終わりに

誰かの役に立つことができていれば幸いです。
アウトプットを頑張ろうと思っているので温かい目で見ていただけると嬉しいです。

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