Swift PlaygroundsでDragGesture、RotationGestureを学ぶ

SwiftUI

概要:


DragGesture、RotationGestureをソースコードから学びます。

iPadのアプリSwift Playgroundsの中の「ジェスチャの認識」のソースコードで説明します。

「ジェスチャの認識」のファイル構成やTapGesture/LongPressGestureに関しては前回の記事で書いていますので、よろしければご参照ください。

DragGesture

これはGestureの中で最もマスターしておきたいところですね。

以下がDragViewのソースコードです。

struct DragView: View {
    private let circleSize: CGFloat = 100
    @State private var offset = CGSize.zero
    var dragGesture: some Gesture {
        DragGesture()
            .onChanged { value in
                offset = CGSize(width: value.startLocation.x + value.translation.width - circleSize/2,
                                height: value.startLocation.y + value.translation.height - circleSize/2)
            }
    }

    var body: some View {
        VStack {
            Text("Use one finger to drag the circle around")
            Spacer()
            Circle()
                .foregroundColor(.teal)
                .frame(width: circleSize, height: circleSize)
                .offset(offset)
                .gesture(dragGesture)
            Spacer()
        }
        .navigationTitle("Drag")
        .padding()
        .toolbar {
            Button("Reset") {
                offset = .zero
            }
        }
    }
}

Viewの構成はTapGestureやLongPressGestureと同じですね。

ここでは.onChangedを使っています。押されている間は移動(座標の変化)があるとvalue構造体に最新の値がセットされて、常に呼ばれるイメージです。

value構造体は主に以下のようなプロパティを持っています。

var startLocationCGPointドラッグをスタートした時の位置
var locationCGPointドラッグ中の現在の位置
var translationCGSizeスタート位置から現在位置の移動距離
DragGesture.Valueの主なプロパティ

注意しなければならないのは、startLocationやlocationはデバイスの画面での座標ではなく、Gestureを設定したViewの左上からの座標になります。

今回の場合、初期に円の中心をタッチすると(50,50)の座標がstartLocationに入ってきます。
なので計算式では半円分の値を引いているのですね。

.onChanged { value in
    offset = CGSize(width: value.startLocation.x + value.translation.width - circleSize/2,
                                height: value.startLocation.y + value.translation.height - circleSize/2)
}

ただ、この計算式だと円の端をドラッグすると不自然な動きです(笑)

参考までに、これを改善したソースコードを紹介しておきます。

struct DragView: View {
    private let circleSize: CGFloat = 100
    @State private var offset = CGSize.zero
    @State private var lastOffset = CGSize.zero  // ← 追加

    var dragGesture: some Gesture {
        DragGesture()
            .onChanged { value in
                offset = CGSize(width: value.translation.width + lastOffset.width,  // 変更
                                          height: value.translation.height + lastOffset.height)  // 変更
            }
            .onEnded { _ in            // ← 追加 
                lastOffset = offset.  // ← 追加
            }                                     // ← 追加      
    }
    // 以下省略

ドラッグ終了時にその時のoffsetを保持し、次回はそこからの差分にしています。

DragGestureにPathを組み合わせる

同じDragGestureを使っているSingleLineのViewを説明します。
アプリ内ではLIne Drawingという名称になっている部分です。

ソースコードは以下の通りです。

struct SingleLine: View {
    @State var lineStart = CGPoint.zero
    @State var lineEnd = CGPoint.zero
    var lineDrawingGesture: some Gesture {
        DragGesture()
            .onChanged { value in
                lineStart = value.startLocation
                lineEnd = value.location
            }
            .onEnded { value in
                lineEnd = value.location
            }
    }
    
    var body: some View {
        VStack {
            Text("Touch and drag to make a line")
            Spacer()
            Path { path in
                path.move(to: lineStart)
                path.addLine(to: lineEnd)
            }
            .stroke(Color.green, lineWidth: 8.0)
            .contentShape(Rectangle())
            .gesture(lineDrawingGesture)
        }
        .navigationTitle("Line Drawing")
        .padding()
        .toolbar {
            Button("Reset") {
                lineStart = .zero
                lineEnd = .zero
            }
        }
    }
}

onChangedは、ドラッグを開始してから終了するまで常に呼び出されます。その時の位置情報がvalue構造体にセットされて来ます。

onEndedは、ドラッグが終了する最終の時だけ呼ばれます。

SwiftUIでは、@Stateで宣言しているプロパティが変化すると、関連Viewへ通知されます。

lineStartやlineEndが変化すると、都度Pathがリアルタイムに線を描くわけです。

Pathを簡単に説明しておくと、
.moveは、描き初めの点へ「筆」を持っていく(まだ書かない)イメージ。
.addLineは、move位置から筆を下ろし、toまで線を引く。
.strokeは、線を描くときの色と幅を指定する。
.contentShapeは、描画を描き始められる場所の範囲の形を指定する。
という感じです。

色々変えて、試してみると理解が早まると思います。

RotationGesture

最後はRotationGestureを見ていきます。
リストから”Rotate”を選択した画面です。

2本指で図形を回転されることができるというアプリです。では、ソースコードを見ていきましょう。

struct RotateView: View {
    @State private var rotation = Angle.zero

    var rotationGesture: some Gesture {
        RotationGesture()
            .onChanged{ angle in
                rotation = angle
            }
            .onEnded { angle in
                rotation = angle
            }
    }
    
    var body: some View {
        VStack {
            Text("Use two fingers to rotate the box")
            Spacer()
            Rectangle()
                .foregroundColor(.red)
                .frame(width: 225, height: 225)
                .rotationEffect(rotation)
                .gesture(rotationGesture)
            Spacer()
        }
        .navigationTitle("Rotate")
        .padding()
        .toolbar {
            Button("Reset") {
                rotation = .zero
            }
        }
    }
}

これもいたってシンプルですね。RotationGestureのonChangedやonEndedで受け渡されるのはangleです。それをRectangleのrotationEffectに渡しているだけです。

Angleはラジアンで扱います。値は0から2πということになります。onChangedやonEndedで出てくるのも、rotationEffectに渡すのもラジアンです。

onChangedで渡されるangleはその都度の途中経過、
onEndedで渡されるangleが最終確定値、
になりますので、角度を正しく読みたい場合はonEndedの値を使います。

最後に

iPadのアプリSwift Playgroundsにある「ジェスチャの認識」というアプリのソースコードで、ジェスチャ系の解説してきました。

ジェスチャの基本的なところが、とてもわかりやすく学べるサンプルプログラムだと思います。

ここまで読んでいただき、ありがとうございました。

ご意見、ご指摘等ありましたら、コメント頂けると大変嬉しいです。

コメント

タイトルとURLをコピーしました