こんにちは!iOSアプリを開発している澁谷太智です!
業務内で、楽曲プレイヤー画面を作成していました。 下記のような要件の画面を作成していました。
- 縦スクロールで楽曲プレイヤーを閉じることができる
- 画面上部に、横スクロールで曲送り戻しのできるジャケット写真を表示する
- 再生位置を表示・調整できるスライダーを表示する
この画面を作成する際に、縦スクロール/横スクロールを SwiftUI の Gesture を用いて実装しました。 この2つの Gesture を共存させるのに苦労しました。 今回はその方法について解説していこうと思います!
ニッチな内容かとは思いますが、お付き合いください!
目次
下準備
まず、下のGIFのように動作するViewを作成します。

View構成
最下層から順に下記のようになっています。
- ZStack
- Rectangle(色:indigo)
- VStack
- HorizontalMoveView
- Rectangle(色:orange)
- HorizontalMoveView
- Rectangle(色:mint)
コード
親View
- ZStack を画面いっぱいに作成
- 適当な大きさの
Rectangle()を ZStack 内に配置 - Rectangle の上に
VStackを配置 - VStack 内に後ほど作成する
HorizontalMoveView()を大きさ違いで2つ作成 - 縦方向にドラッグできるジェスチャーを作成
- ZStack に対して作成したジェスチャーを設定

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 に対して設定する

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で親ViewのGestureが発火しない | 全ての子Viewで親ViewのGestureが発火する | 特定の子Viewのみ親ViewのGestureが発火する |
作成したコードを改修していきます。
解決策
全ての子Viewで親ViewのGestureと共に動作させる
下記1点の変更を加えることで解決します。
- ZStack に設定していた、
.gesture()を.simultaneousGesture()へ変更する

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)に変更する

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 のように見せる実装が一つの手段として考えられます。同じ状況に置かれた方の助けになればと思います。
最後まで読んでいただき、ありがとうございました!
また別の記事でお会いしましょう。
澁谷太智


