[SwiftUI] 複数ジェスチャーのサポート (Pinch と Drag の両方をサポートする)

SwiftUI2021

DragGesture と MagnificationGesture の両方をサポートする方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.4
  • Xcode 13 beta
  • iOS 15 beta
MEMO
Xcode13 beta, iOS 15 beta で確認していますが、Xcode12, iOS14 でも動作します。

DragGesture と MagnificationGesture

DragGesture

DragGesture を使うことで、画面上でのドラッグ操作に応じた動作を実装することができます。

Apple のドキュメントは、こちら

ドラッグ操作に対応するビューに View Modifier として 指定します。

以下のような動作になります。

コードでは、以下のように、.gesture 指定しています。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/18
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    // (1)
    @State var offset:CGSize = .zero
    @State var initialOffset: CGSize = .zero

    var body: some View {
        // (2)
        let dragGesture = DragGesture()
            .onChanged { offset = CGSize(width: initialOffset.width + $0.translation.width, height: initialOffset.height + $0.translation.height) }
            .onEnded{ _ in initialOffset = offset }
        return VStack {
            ZStack {
                Circle()
                    .fill(Color.red)
                    .frame(width:100, height:100)
                Rectangle()
                    .fill(.blue)
                    .frame(width: 50, height: 40)
            }
            // (3)
            .offset(offset)
            // (4)
            .gesture(dragGesture)
            Button(action: {
                offset = .zero; initialOffset = .zero
            }, label: { Text("Reset") } )
        }
    }
}
コード解説
  1. 表示オフセット量、ドラッグ開始時のオフセット量 向けの変数定義をします
  2. ドラッグ中に検知した移動量を含めたオフセット量をセットします、ドラッグ終了時には、オフセット量の最終値を記録します
  3. .offset を使用して、ビューの表示位置を移動させます
  4. .gesture を使用して、ビューに対応する gesture を指定します

MagnificationGesture

MagnificationGesture を使うことで、ピンチ操作に応じた動作を実装することができます。

Apple のドキュメントは、こちら

ピンチ操作に対応するビューに View Modifier として 指定します。

以下のような動作になります。

コードでは、以下のように、.gesture 指定しています。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/18
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    // (1)
    @State var scale:CGFloat = 1.0
    @State var initialScale: CGFloat = 1.0

    var body: some View {
        // (2)
        let magnificationGesture = MagnificationGesture()
            .onChanged { scale = $0 * initialScale }
            .onEnded{ _ in initialScale = scale }
        return VStack {
            ZStack {
                Circle()
                    .fill(Color.red)
                    .frame(width:100, height:100)
                Rectangle()
                    .fill(.blue)
                    .frame(width: 50, height: 40)
            }
            // (3)
            .scaleEffect(scale)
            // (4)
            .gesture(magnificationGesture)
            Button(action: {
                scale = 1.0; initialScale = 1.0
            }, label: { Text("Reset") } )
        }
    }
}
コード解説
  1. スケール値と、初期スケール値を保持するための変数定義
  2. ピンチ操作から取得できるスケール値を使ってセットします、ピンチ操作終了時に、最終的なスケール値を記録します
  3. .scaleEffect を使用します
  4. .gesture を使用して、ビューに対応する gesture を指定します

複数の Gesture を指定する方法

ドラッグとピンチの両方に対応しようと考え、.gesture を複数使って、gesture 指定しても、期待する動作になりません。.gesture を複数つけると、最初に指定した gesture が有効となり、以降の gesture は、無視されてしまいます。

SimultaneousGesture

SimultaneousGesture を使うことで複数の gesture をまとめて1つの gesture として定義することもできます。

Apple のドキュメントは、こちら

以下のように使います。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/18
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State var offset:CGSize = .zero
    @State var initialOffset: CGSize = .zero
    @State var scale:CGFloat = 1.0
    @State var initialScale: CGFloat = 1.0

    var body: some View {
        let magnificationGesture = MagnificationGesture()
            .onChanged { scale = $0 * initialScale }
            .onEnded{ _ in initialScale = scale }
        let dragGesture = DragGesture()
            .onChanged { offset = CGSize(width: initialOffset.width + $0.translation.width, height: initialOffset.height + $0.translation.height) }
            .onEnded{ _ in initialOffset = offset }
        return VStack {
            GeometryReader { geom in
                ZStack {
                    Circle()
                        .fill(Color.red)
                        .frame(width:100, height:100)
                    Rectangle()
                        .fill(.blue)
                        .frame(width: 50, height: 40)
                }
                .offset(offset)
                .scaleEffect(scale)
            }
            // (1)
            .gesture(SimultaneousGesture(magnificationGesture, dragGesture))
            Button(action: {
                scale = 1.0; initialScale = 1.0
                offset = .zero; initialOffset = .zero
            }, label: { Text("Reset") } )
        }
    }
}
コード解説
  1. SimultaneousGesture を使い、magnificationGesture と dragGesture を1つにして、.gesture 指定しています

.simultaneousGesture

.simultaneousGesture という View Modifier を使用すると 複数の gesture を優劣なく同等に設定することができます

SimultaneousGesture では、複数のジェスチャーを1つのジェスチャーにまとめてから ビューに指定しますが、.simultaneousGesture では、階層関係にあるビューのジェスチャーに対しても有効となります。

もちろん、1つのビューに対して .gesture と .simultaneousGesture を指定しても有効です。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/18
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State var offset:CGSize = .zero
    @State var initialOffset: CGSize = .zero
    @State var scale:CGFloat = 1.0
    @State var initialScale: CGFloat = 1.0

    var body: some View {
        let magnificationGesture = MagnificationGesture()
            .onChanged { scale = $0 * initialScale }
            .onEnded{ _ in initialScale = scale }
        let dragGesture = DragGesture()
            .onChanged { offset = CGSize(width: initialOffset.width + $0.translation.width, height: initialOffset.height + $0.translation.height) }
            .onEnded{ _ in initialOffset = offset }
        return VStack {
            ZStack {
                Circle()
                    .fill(Color.red)
                    .frame(width:100, height:100)
                Rectangle()
                    .fill(.blue)
                    .frame(width: 50, height: 40)
            }
            .offset(offset)
            .scaleEffect(scale)
            .gesture(dragGesture)
       // (1)
            .simultaneousGesture(magnificationGesture)
            Button(action: {
                scale = 1.0; initialScale = 1.0
                offset = .zero; initialOffset = .zero
            }, label: { Text("Reset") } )
        }
    }
}
コード解説
  1. .gesture 指定した dragGesture に加えて、.simulataneousGesture を使って、magnificationGesture も指定しています

シミュレータ上でのピンチ操作

実機であれば、タッチ操作でそのまま実行できますが、simulator を使った時の操作方法も説明しておきます。

Option キー

Option キーを押下すると、画面中心を点対称とした2つのグレーの円が表示されます。この円が、ピンチ等の時のタッチ位置を表しています。

この時点では、まだ画面にタッチしていませんので、Option キーを 押したままの状態で マウス操作することで、2点の位置関係を調整することができます。

Shift キー

(Option キーを押したままで) Shift キーを押下することで、2点の中心点を移動させることができます。

先ほどは、画面中央を中心とした2点でしたが、画面中央以外の点を中心にすることができるようになります。

左クリック

上記の Option キーと Shift キーを使って、タッチ位置を調整した後に、Option キーを離さずに マウスを左クリックすることで、ピンチ操作を実行することができます。

現時点での制限

現時点の SwiftUI で提供される MaginificationGesture では、ピンチ動作時のスケール値を取得することはできますが、座標を取得することはできません。

スケール値は、取得できますので、その値を基準にスケール表示することはできますが、スケールする際の中心点をどこにするべきかの情報を入手できません。

普通にスケールをかけるだけであれば問題ありませんが、移動と拡大を組み合わせると違和感を感じる実装になってしまいます。

現時点では、MaginifcationGesture を使う限り、回避する方法は無いようです。

まとめ:複数ジェスチャーのサポート

複数ジェスチャーのサポート
  • SimultaneousGesture を使うと複数のジェスチャーを組み合わせたジェスチャーを定義できる
  • .simulataneousGesture を使うと、複数のジェスチャーを組み合わせた動作を可能にできる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です