【SwiftUI】ピンチイン・ピンチアウトでViewを拡大縮小する

2022.04.07

iPhoneユーザーなら誰しもピンチインアウトでViewを拡大縮小したい時が人生に一度や二度あると思いますが、僕もその時がやってきたので調べてみることにしました。

作ったもの

環境

  • Xcode 13.3

MagnificationGesture

Viewを拡大縮小する為には、MagnificationGestureというジェスチャーを使用する必要があります。

Apple公式ドキュメント: MagnificationGestureに記載されているコードと実行結果のデモを載せておきました。

コード

struct MagnificationGestureView: View {

    @GestureState var magnifyBy = 1.0

    var magnification: some Gesture {
        MagnificationGesture()
            .updating($magnifyBy) { currentState, gestureState, transaction in
                gestureState = currentState
            }
    }

    var body: some View {
        Circle()
            .frame(width: 100, height: 100)
            .scaleEffect(magnifyBy)
            .gesture(magnification)
    }
}

@GestureStateはジェスチャーを実行している間にプロパティを更新し、ジェスチャーが終了するとプロパティを初期状態にリセットするプロパティラッパータイプです。 updating(_:body:)では、第一引数にバインディングする為のGestureStateを渡して、bodyは、ジェスチャーの値が変更されたときに呼ばれるコールバックで、ジェスチャー更新後の状態の値が渡ってきます。

ドキュメントにも記載していますが、この実装だとジェスチャーが終わると元の大きさに戻ってしまいます。

デモ

ジェスチャーが終了しても大きさを元の戻らないようにする

今回はジェスチャーを終了しても大きさを元に戻したくなかったので、元に戻らないように実装していきたいます。

MagnificationGesture.Valueは毎回1.0からスタートする

@GestureStateだとジェスチャー後に元の大きさに戻ってしまう為、@Stateに変更します。

onChangeを使用して、変更後のMagnificationGesture.Value(拡大縮小の倍率)を受け取り、magnifyByの値を更新後の倍率で更新するような実装にしたところ、MagnificationGesture()が開始された地点を1.0としてそこからどのくらい縮小されたか、拡大されたかの倍率がMagnificationGesture.Valueとして値が渡ってくることが分かりました。

ジェスチャー内のコードを下記に変更して動きを確認します。

@State private var magnifyBy = 1.0

var magnification: some Gesture {
    MagnificationGesture()
        .onChanged { value in
            magnifyBy = value
        }
}

デモ

ピンチアウトを行い、ピンチアウトを終了すると大きさは維持されますが、もう一度MagnificationGesture()を開始したタイミングでmagnifyBy1.0の値が代入される為、元の大きさに戻っています。

大きさが戻らないように実装する

ジェスチャー部分とプロパティ部分をこのように置き換えました。

@State private var magnifyBy = 1.0
@State private var lastMagnificationValue = 1.0

var magnification: some Gesture {
    MagnificationGesture()
        .onChanged { value in
            // 前回の拡大率に対して今回の拡大率の割合を計算
            let changeRate = value / lastMagnificationValue
            // 前回からの拡大率の変更割合分を乗算する
            magnifyBy *= changeRate
            // 前回の拡大率を今回の拡大率で更新
            lastMagnificationValue = value
        }
        .onEnded { value in
            // 次回のジェスチャー時に1.0から始まる為、終了時に1.0に変更する
            lastMagnificationValue = 1.0
        }
}

ジェスチャーの実装について説明していきたいと思います。

前回の拡大率に対して今回の拡大率の割合を計算

今回の拡大率valueが前回の拡大率に対してどのくらい変化したかの割合を出しています。

let changeRate = value / lastMagnificationValue

前回からの拡大率の変更割合分を乗算する

前回からの変更割合分をmagnifyByに乗算します。

magnifyBy *= changeRate

前回の拡大率を今回の拡大率で更新

変化率の割合を計算する為に使用しているlastMagnificationValueを今回のMagnificationGesture.Valueの値で更新します。

lastMagnificationValue = value

ジェスチャー終了時に前回の拡大率を初期化

ジェスチャーが終了した時に呼ばれるonEndedの実装になります。

次回のジェスチャー開始時にはMagnificationGesture.Value1.0から始まる為、lastMagnificationValueの値も1.0にしておきます。

.onEnded { value in
    // 次回のジェスチャー時に1.0から始まる為、終了時に1.0に変更する
    lastMagnificationValue = 1.0
}

これでピンチイン・ピンチアウトを行なった際に拡大率を維持したまま、表現することが出来るようになりました。

おまけ

作ったもので載せているデモでは、Apple公式から参考に変更していったコードだと拡大する対象がただの黒丸で味気なかったので、Apple標準で用意してくれているPlayStationのロゴを使っています。

実際のアプリでこのシンボルを使用する時は、PlayStationに関連する時にしか使用出来ず、 ロゴに変更は加えることはできないという使用制限があるのでご注意下さい。

var body: some View {
    Image(systemName: "logo.playstation")
        .resizable()
        .frame(width: 100, height: 77.5)
        .scaleEffect(scaleValue)
        .gesture(magnification)
}

おわりに

PS5のキャッチコピーを調べたら、PLAY HAS NO LIMITSと出てきて、とてもかっこいいと思いました。

私もYATTEMITA HAS NO LIMITSの精神で邁進していきたいと思います。

参考