It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【SwiftUI】ピンチによるViewの拡大と縮小

概要

SwiftUIを使ってピンチイン・ピンチアウトによってView(画像に関わらずViewコンポーネント)を拡大・縮小する方法について記載します。

環境

  • Swift: 5.2.4
  • Xcode: 11.6

サンプル環境

f:id:inon29:20200815130637p:plain

struct ContentView: View {
    var body: some View {
        ZStack {
            Circle()
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
        }
    }
}

今回は、画面に円を一つ配置し、この円をピンチジェスチャーで拡大/縮小します。

ピンチジェスチャーの取得

SwiftUIのViewコンポーネントには、gesture というModifierが用意されています。
このgestureというModifierを使うとタップやドラッグ、ピンチといったジェスチャーを簡単にトラッキングすることができます。

struct ContentView: View {
    var body: some View {
        ZStack {
            Circle()
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
                .scaleEffect(self.scale)
                .gesture(MagnificationGesture()
                    .onChanged { value in
                        print("onChanged: ", value)
                    }
                    .onEnded { value in
                        print("onEnded: ", value)
                    }
                )
        }
    }
}

ピンチのジェスチャーを取得するには、 MagnificationGesture()gesture Modifierに渡します。
Gestureコンポーネントには、「値の変更」をコールバックする onChanged、「イベントの完了」をコールバックする onEndedなどイベント検知のModifierが用意されており、これらをセットすることによりジェスチャーの値を取得できます。
MagnificationGestureを使用した場合、onChanged、onEndedに渡される value には、現在の拡大倍率を基準とした拡大率(縮小率)がCGFloat型で取得できます。

Viewを拡大・縮小する

struct ContentView: View {
    @State var scale: CGFloat = 1.0
    var body: some View {
        ZStack {
            Circle()
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
                .scaleEffect(self.scale)
                .gesture(MagnificationGesture()
                    .onChanged { value in
                        self.scale = value
                    }
                    .onEnded { value in
                        print("onEnded: ", value)
                    }
                )
        }
    }
}

拡大率は取得できたのでこれを使用して、円のView自体をピンチジェスチャーに合わせて拡大・縮小します。
Viewの拡大・縮小を行うには、scaleEffect というModifierを使います。scaleEffectに元の大きさを1.0とした拡大率をセットすることでViewを拡大(縮小)することができます。
先程記述した、MagnificationGestureのonChangedで変更倍率を受け取り、@Stateで宣言したscale変数に値をセットします。
そして、scale変数をscaleEffectにセットすることで、ピンチジェスチャーの倍率変更に合わせてViewを拡大(縮小)することができます。

変更倍率を保持する

上記までの処理を動かすと一見上手く動いているように見えますが、今は ピンチジェスチャーを行うたびに画像の大きさが1.0倍に一旦戻ってしまうという状態になっています。

これは、onChanged, onEnded で取得できる value が 「現在のサイズからの拡大率」であるためです。
(元画像から2倍に拡大されていても、ピンチジェスチャーが一度終わっていれば次のピンチジェスチャーでは1.0から始まる)

これだと使いにくいため、ピンチジェスチャーが複数回行われても、元画像の拡大率を維持した状態になるように変更します。

struct ContentView: View {
    @State var lastValue: CGFloat = 1.0
    @State var scale: CGFloat = 1.0
    var body: some View {
        ZStack {
            Circle()
                .foregroundColor(Color.blue)
                .frame(width: 200, height: 200)
                .scaleEffect(self.scale)
                .gesture(MagnificationGesture()
                    .onChanged { value in
                        // 前回の値から拡大率を計算する
                        let delta = value / self.lastValue
                        // 現在の拡大率を再設定する
                        self.scale = self.scale * delta
                        // 現在の拡大率を覚えておく
                        self.lastValue = value
                    }
                    .onEnded { value in
                        // 次のジェスチャーイベントではvalueはまた1.0から始まるため
                        // ジェスチャーイベント完了時に1.0に戻しておく
                        self.lastValue = 1.0
                    }
                )
        }
    }
}

上記が変更後のコードです。

onChangevalueをそのままscaleにセットするのではなく、(同一ジェスチャーイベントでの)前回のvalue(lastValue)との拡大率の変更を計算してscaleにセットしています。
またジェスチャーイベントで取得できるvalueは、毎回1.0から始まるため、onEndedlastValue変数を1.0に初期化しています。

このようにすることで、拡大率をリセットさせずにピンチジェスチャーによる拡大・縮小を行えます。