【Swift UI】@GestureStateの使い方!updating(_:body:)メソッド

この記事からわかること
- Swift UIの@GestureStateの使い方
- ドラッグ(スワイプ)したビューを移動させる方法
- updating(_:body:)メソッドの使い方
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.3
- iOS:18.0
- Swift:5.9
- macOS:Sequoia 15.4
Swift UIでのGestureの実装方法については以下の記事を参考にしてください。
@GestureStateとは?
Swift UIで使える@GestureState
はジェスチャーの状態を管理するためのプロパティラッパーです。このプロパティラッパーを付与することでジェスチャーが変更されるたびに自動的に更新された値をそのプロパティから参照することができます。
@GestureState
を付与したプロパティはGesture.updating(_:body:)
メソッドでバインディングすることができるようになるのでセットで使用することが多いです。
@GestureState private var dragOffset: CGFloat = 0
updating(_:body:)メソッド
公式リファレンス:updating(_:body:)メソッド
func updating<State>(
_ state: GestureState<State>,
body: @escaping (Self.Value, inout State, inout Transaction) -> Void
) -> GestureStateGesture<Self, State>
updating(_:body:)
メソッドはジェスチャーが変更されるたびに呼び出されるクロージャーを提供し、ジェスチャーの状態を更新するために使用されます。引数state
には@GestureState
を付与したプロパティをバインディングし、 ジェスチャーの状態が変化したタイミングで引数body
のクロージャーが呼び出され、その引数からジェスチャーの値、状態(GestureState)、およびトランザクションを参照することが可能です。
使い方
では@GestureState
を使用してViewをドラッグで移動させるように実装してみます。
ドラッグされたビューを動かすためのポイント
- @GestureStateでドラッグ量を保持
- updating(_:body:)でドラッグジェスチャーの変更を観測&反映
- offsetでViewを移動
struct DragView: View {
// Viewを表示させる座標値
@State private var position = CGPoint(x: 0, y: 0)
// ドラッグジェスチャー量
@GestureState private var dragOffset = CGSize.zero
var body: some View {
Text("Hello")
.frame(width: 100, height: 30)
.background(Color.gray)
// Viewを実際に移動させる
.offset(x: position.x + dragOffset.width, y: position.y + dragOffset.height)
.gesture(
DragGesture()
// ドラッグジェスチャーの変更を観測
.updating($dragOffset, body: { (value, state, _) in
// state(GestureState)に移動した値を格納することで変化中も移動する
state = value.translation
})
// ドラッグ終了した際に呼ばれる
.onEnded({ value in
// 移動が完了した座標を格納して固定
self.position.x += value.translation.width
self.position.y += value.translation.height
})
)
}
}

バナーコンテナビューを実装する
GestureState
を使用して「バナーコンテナビューを実装」してみました。スワイプでスクロールできて、バナーごとに停止するようになっています。

struct BannerContainerView: View {
private let array = Array(1...30)
private let deviceWidth = DeviceSizeUtility.deviceWidth
@GestureState private var dragOffset: CGFloat = 0
@State private var currentIndex: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
GeometryReader { geometry in
LazyHStack(spacing: 0) {
ForEach(array, id: \.self) { value in
Text("\(value)")
.frame(width: deviceWidth, height: 80)
.background(.orange)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke()
}
}
}
}
// スワイプ中にバナーコンテナを移動させるためのオフセット
.offset(x: dragOffset)
// スワイプ完了後にバナーコンテナ自体を移動した後に固定するためのオフセット
.offset(x: -(currentIndex * deviceWidth))
// スワイプ完了後の動作をなめらかにするためのアニメーション
.animation(.interpolatingSpring(mass: 0.6, stiffness: 150, damping: 80, initialVelocity: 0.1), value: dragOffset)
.gesture(
DragGesture(minimumDistance: 0)
// スワイプの変化を観測しスワイプの変化分をHStackのoffsetに反映(スワイプでビューが動く部分を実装)
.updating(self.$dragOffset, body: { (value, state, _) in
// スワイプ変化量をdragOffsetに反映
state = value.translation.width
// スワイプが完了するとdragOffsetの値は0になる
print("START", dragOffset)
})
.onEnded { value in
// value.translation.width:ドラッグを離した時の最終的な移動距離
// 1なら左スワイプ
// 0なら右スワイプ
let newIndex: CGFloat = value.translation.width > 0 ? 1 : 0
if newIndex == 1 {
currentIndex = max(currentIndex - 1, 0)
print("右スワイプ", currentIndex)
} else {
currentIndex = min(currentIndex + 1, CGFloat(array.count))
print("左スワイプ", currentIndex)
}
print("END", dragOffset)
}
)
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。