目次

目次

【SwiftUI】Gestureで縦横スクロールを共存させる

アバター画像
澁谷太智
アバター画像
澁谷太智
最終更新日2025/12/25 投稿日2024/09/30

こんにちは!iOSアプリを開発している澁谷太智です!

業務内で、楽曲プレイヤー画面を作成していました。 下記のような要件の画面を作成していました。

  • 縦スクロールで楽曲プレイヤーを閉じることができる
  • 画面上部に、横スクロールで曲送り戻しのできるジャケット写真を表示する
  • 再生位置を表示・調整できるスライダーを表示する

この画面を作成する際に、縦スクロール/横スクロールを SwiftUI の Gesture を用いて実装しました。 この2つの Gesture を共存させるのに苦労しました。 今回はその方法について解説していこうと思います!

ニッチな内容かとは思いますが、お付き合いください!

目次

  1. 下準備
  2. 問題
  3. 解決策
    1. 全ての子Viewで親ViewのGestureと共に動作させる
    2. 特定の子Viewでのみ親ViewのGestureと共に動作させる
  4. 最後に

下準備

まず、下のGIFのように動作するViewを作成します。

prepare

View構成

最下層から順に下記のようになっています。

  • ZStack
    • Rectangle(色:indigo)
    • VStack
    • HorizontalMoveView
      • Rectangle(色:orange)
    • HorizontalMoveView
      • Rectangle(色:mint)

コード

親View

  • ZStack を画面いっぱいに作成
  • 適当な大きさの Rectangle() を ZStack 内に配置
  • Rectangle の上に VStack を配置
  • VStack 内に後ほど作成する HorizontalMoveView() を大きさ違いで2つ作成
  • 縦方向にドラッグできるジェスチャーを作成
  • ZStack に対して作成したジェスチャーを設定
image-20240930161702332
ContentView.swift
struct ContentView: View {
    @State private var zStackYOffset: CGFloat = .zero
    @State private var lastYOffset: CGFloat = .zero

    var body: some View {
        let dragGesture = DragGesture()
            .onChanged { value in
                zStackYOffset = lastYOffset + value.translation.height
            }
            .onEnded { value in
                zStackYOffset = lastYOffset + value.translation.height
                lastYOffset = zStackYOffset
            }
        ZStack() {
            Rectangle()
                .fill(.indigo)
                .frame(width: .infinity, height: 600)
            VStack {
                HorizontalMoveView(color: .orange)
                    .frame(width: 300, height: 300)
                HorizontalMoveView(color: .mint)
                    .frame(width: 100, height: 100)
            }
        }
        .offset(y: zStackYOffset) // ドラッグに応じてY軸のOffsetが変化する
        .gesture(dragGesture) // ジェスチャーを設定
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

子View(HorizontalMoveView)

  • 適当な大きさの Rectangle() を作成する
  • 横方向にドラッグできるジェスチャーを Rectangle に対して設定する
image-20240930161848038
HorizontalMoveView.swift
struct HorizontalMoveView: View {
    /// RectangleのX軸Offset
    @State private var rectXOffset: CGFloat = .zero
    /// Dragした後の最終的なOffset
    @State private var lastXOffset: CGFloat = .zero
    /// Rectangleの色
    private let color: Color

    init(color: Color) {
        self.color = color
    }

    var body: some View {
        Rectangle()
            .fill(color)
            .offset(x: rectXOffset) // ジェスチャーを設定
            .gesture( // ドラッグに応じてX軸のOffsetが変化する
                DragGesture()
                    .onChanged { value in
                        rectXOffset = lastXOffset + value.translation.width
                    }
                    .onEnded { value in
                        rectXOffset = lastXOffset + value.translation.width
                        lastXOffset = rectXOffset
                    }
            )
            .border(.cyan)
    }
}

問題

触ってみるとわかるのですが、親Viewの縦方向の DragGesture が子View上では動作しません。

今回の期待値である、

  • 全ての子Viewで親Viewの Gesture と共に動作させる
  • 特定の子Viewでのみ親Viewの Gesture と共に動作させる

が満たせていないことがわかります。

作成した画面の動き 期待値 1 期待値 2
view_made
expectation_1
expectation_2
子Viewで親ViewのGestureが発火しない 全ての子Viewで親ViewのGestureが発火する 特定の子Viewのみ親ViewのGestureが発火する

作成したコードを改修していきます。

解決策

全ての子Viewで親ViewのGestureと共に動作させる

下記1点の変更を加えることで解決します。

  • ZStack に設定していた、.gesture().simultaneousGesture() へ変更する
image-20240930162022545
ContentView.swift
struct ContentView: View {
    @State private var zStackYOffset: CGFloat = .zero
    @State private var lastYOffset: CGFloat = .zero

    var body: some View {
        let dragGesture = DragGesture()
            .onChanged { value in
                zStackYOffset = lastYOffset + value.translation.height
            }
            .onEnded { value in
                zStackYOffset = lastYOffset + value.translation.height
                lastYOffset = zStackYOffset
            }
        ZStack() {
            Rectangle()
                .fill(.indigo)
                .frame(width: .infinity, height: 600)
            VStack {
                HorizontalMoveView(color: .orange)
                    .frame(width: 300, height: 300)
                HorizontalMoveView(color: .mint)
                    .frame(width: 100, height: 100)
            }
        }
        .offset(y: zStackYOffset)
        .simultaneousGesture(dragGesture) // .simultaneousGesture()でジェスチャーを設定
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

特定の子Viewでのみ親ViewのGestureと共に動作させる

下記2点の変更を加えることで解決します。

  • 親View内の HorizontalMoveView() に対して、.simultaneousGesture() で親Viewの Gesture を設定する
  • 親Viewの Gesture のイニシャライズを、DragGesture(coordinateSpace: .global) に変更する
image-20240930162220141
ContentView.swift
struct ContentView: View {
    @State private var zStackYOffset: CGFloat = .zero
    @State private var lastYOffset: CGFloat = .zero

    var body: some View {
        let dragGesture = DragGesture(coordinateSpace: .global) // coordinateSpaceを.global に設定
            .onChanged { value in
                zStackYOffset = lastYOffset + value.translation.height
            }
            .onEnded { value in
                zStackYOffset = lastYOffset + value.translation.height
                lastYOffset = zStackYOffset
            }
        ZStack() {
            Rectangle()
                .fill(.indigo)
                .frame(width: .infinity, height: 600)
            VStack {
                HorizontalMoveView(color: .orange)
                    .frame(width: 300, height: 300)
                    .simultaneousGesture(dragGesture) // .simultaneousGesture()でジェスチャーを設定
                HorizontalMoveView(color: .mint)
                    .frame(width: 100, height: 100)
            }
        }
        .offset(y: zStackYOffset)
        .gesture(dragGesture)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

coordinateSpace のデフォルト値は .local です。

local のまま HorizontalMoveView に対して Gesture を設定すると、HorizontalMoveView の 座標を基準として計算を行なってしまいます。親Viewと同じ計算座標にしたいので、 .global で画面全体の座標を基準として計算させるようにしています。

公式ドキュメント:CoordinateSpace | Apple Developer Documentation

.simultaneousGesture()

複数のジェスチャーを同時に認識させることができるモディファイア。

公式ドキュメント:simultaneousGesture(_:including:) | Apple Developer Documentation

最後に

いかがだったでしょうか。

そもそも今回の場合、DragGesture と ScrollView(.horizontal) を利用すれば解決なのでは?となるかと思います。

ScrollView の持っている機能だけで開発要件を満たせるのであれば、ScrollView を利用する形で問題ないと思います。(ジェスチャー方向の制御等複雑な処理を書かなくて済むので)

しかしながら、機能要件によっては ScrollView の持っている機能だけでは制御しきれない場合もあります。

そのような場合に、DragGesture を利用して Offset を調節して ScrollView のように見せる実装が一つの手段として考えられます。同じ状況に置かれた方の助けになればと思います。

最後まで読んでいただき、ありがとうございました!

また別の記事でお会いしましょう。

アバター画像

澁谷太智

目次