LoginSignup
1
1

[SwiftUI]Gestureを知る!

Posted at

SwiftUI初心者がGestureを一から調べてみた過程を記録します!
これから学ぼうと思う方にとって有益な情報となれば嬉しいです:)

Add gesture modifiers to a view

Each gesture you add applies to a specific view in the view hierarchy. To recognize a gesture event on a particular view, create and configure the gesture, and then use the gesture(_:including:) modifier:

追加した各ジェスチャは、ビュー階層内の特定のビューに適用されます。 特定のビューでジェスチャ イベントを認識するには、ジェスチャを作成および構成してから、gesture(_:including:) 修飾子を使用します。

struct ShapeTapView: View {
    var body: some View {
        let tap = TapGesture()
            .onEnded { _ in
                print("View tapped!")
            }
        
        return Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(tap)
    }
}

gesture(_:including:)

第一引数がジェネリクス型で、Gestureプロトコルを継承する必要があります。
第二引数には、デフォルト値で.allが設定されています。

func gesture<T>(
    _ gesture: T,
    including mask: GestureMask = .all
) -> some View where T : Gesture

GestureMaskは、特定のジェスチャーがビュー階層のどのレベルで反応するか、または反応しないかを細かく制御するために使用されます。例えば、子Viewにはジェスチャーを反応させないということが可能になります。

Gestureプロトコル

Gestureプロトコルを継承することで、update, onChange, onEndedなどの関数が使えるようになります。

TapGesture, DragGestureなどが、Gestureプロトコルを継承しています。

優先順位

以下は、Imageをタップすると、"Tap on Image."が表示され、四角形をタップすると"Tap on VStack."が表示されます。このように、Viewに直接適用されたジェスチャーが優先されます。

struct GestureExample: View {
    @State private var message = "Message"
    let newGesture = TapGesture().onEnded {
        print("Tap on VStack.")
    }


    var body: some View {
        VStack(spacing:25) {
            Image(systemName: "heart.fill")
                .resizable()
                .frame(width: 75, height: 75)
                .padding()
                .foregroundColor(.red)
                .onTapGesture {
                    print("Tap on image.")
                }
            Rectangle()
                .fill(Color.blue)
        }
        .gesture(newGesture)
        .frame(width: 200, height: 200)
        .border(Color.purple)
    }
}

Respond to gesture callbacks

以下はGesturedプロトコルを継承した型で使える関数です。
TapGestureや、DragGestureなどが当てはまります。

onEnded(_:)

Adds an action to perform when the gesture ends.

func onEnded(_ action: @escaping (Self.Value) -> Void) -> _EndedGesture<Self>

クロージャのバラメーターには、ジェスチャの最終値が含まれます。
Valueの値は、構造体によって違います。
例えば、TapGestureの場合、Valueには、Voidが含まれていました。

public struct TapGesture : Gesture {

    /// The required number of tap events.
    public var count: Int

    /// Creates a tap gesture with the number of required taps.
    ///
    /// - Parameter count: The required number of taps to complete the tap
    ///   gesture.
    public init(count: Int = 1)

    /// The type of gesture representing the body of `Self`.
    public typealias Body = Never

    /// The type representing the gesture's value.
    public typealias Value = Void
}

DragGestureの場合は、timeや、locationなど、様々な値が含まれていました。

public struct DragGesture : Gesture {

    /// The attributes of a drag gesture.
    public struct Value : Equatable, Sendable {

        /// The time associated with the drag gesture's current event.
        public var time: Date

        /// The location of the drag gesture's current event.
        public var location: CGPoint

        /// The location of the drag gesture's first event.
        public var startLocation: CGPoint

        /// The total translation from the start of the drag gesture to the
        /// current event of the drag gesture.
        ///
        /// This is equivalent to `location.{x,y} - startLocation.{x,y}`.
        public var translation: CGSize { get }

        /// The current drag velocity.
        public var velocity: CGSize { get }

        /// A prediction, based on the current drag velocity, of where the final
        /// location will be if dragging stopped now.
        public var predictedEndLocation: CGPoint { get }

        /// A prediction, based on the current drag velocity, of what the final
        /// translation will be if dragging stopped now.
        public var predictedEndTranslation: CGSize { get }

        /// Returns a Boolean value indicating whether two values are equal.
        ///
        /// Equality is the inverse of inequality. For any values `a` and `b`,
        /// `a == b` implies that `a != b` is `false`.
        ///
        /// - Parameters:
        ///   - lhs: A value to compare.
        ///   - rhs: Another value to compare.
        public static func == (a: DragGesture.Value, b: DragGesture.Value) -> Bool
    }
}

onChanged(_:)

Adds an action to perform when the gesture’s value changes.

func onChanged(_ action: @escaping (Self.Value) -> Void) -> _ChangedGesture<Self>

Valueの値が、Equatableを準拠することが必須になります。
パラメーターの値には、ジェスチャの新しい値が含まれます。

updating(_:body:)

Updates the provided gesture state property as the gesture’s value changes.

func updating<State>(
    _ state: GestureState<State>,
    body: @escaping (Self.Value, inout State, inout Transaction) -> Void
) -> GestureStateGesture<Self, State>

bodyは、ジェスチャの値が変化した時に、SwiftUIが呼び出すコールバックです。
Self.Valueには変化後の値が入り、Stateには、変化前の値が入ります。
inoutがついているので、引数でも変更可能です。
Transactionは、ジェスチャによって引き起こされるビューの状態変更のアニメーションや適用の仕方をカスタマイズすることができます。(実際にやってはないので、どうカスタマイズできるのかは不明です‥)

Transactionについて 以下はChatGPTで調べた内容です。

SwiftUIでのupdatingメソッドは、ジェスチャの状態が変化するたびに呼び出されるクロージャを提供します。このクロージャは、ジェスチャの新しい値(Self.Value)、状態を保持するための変数(inout State)、そしてTransactionオブジェクト(inout Transaction)を受け取ります。Transactionオブジェクトには、ビューのアップデートを管理するための情報が含まれています。

Transactionは、SwiftUIのビューの状態の変更がどのようにアニメーション化されるか、またはどのように適用されるかを制御するために使用されます。具体的には、以下のような情報が含まれます:

  • アニメーションTransactionには、ビューの状態変更に適用されるアニメーションが含まれていることがあります。これを設定することで、状態の変化を滑らかにするアニメーションを指定できます。
  • 無効化フラグ:特定のビューの更新を無効化するためのフラグを含むことがあります。これにより、特定の状況下でビューの再描画を防ぐことができます。
  • ディスアビリティグループ:ビューの更新が他の更新と同時に行われるべきかどうかを制御します。

Transactionオブジェクトは、通常、開発者が直接操作することは少なく、SwiftUIのフレームワークによって内部的に使用されます。しかし、updatingメソッド内でTransactionを使用することにより、ジェスチャによって引き起こされるビューの状態変更のアニメーションや適用の仕方をカスタマイズすることができます。例えば、ジェスチャに基づいてビューの状態を更新する際に、特定のアニメーションを適用したり、更新を即時に反映させるためにアニメーションを無効にしたりすることが可能です。

updating使用例 ジェスチャの変化に応じてビューを更新するには、GestureState プロパティをビューに追加し、updateing(_:body:) コールバックで更新します。 SwiftUI は、ジェスチャを認識するとすぐに、またジェスチャの値が変更されるたびに、更新コールバックを呼び出します。 たとえば、SwiftUI は、拡大ジェスチャが開始されるとすぐに更新コールバックを呼び出し、その後は倍率値が変更されるたびに再度呼び出します。 SwiftUI は、ユーザーがジェスチャを終了またはキャンセルしたときに更新コールバックを呼び出しません。 代わりに、ジェスチャ状態プロパティはその状態を自動的に初期値にリセットします。

たとえば、ユーザーが長押ししている間に色が変化するビューを作成するには、ジェスチャ状態プロパティを追加し、更新コールバックでそれを更新します。

struct CounterView: View {
    @GestureState private var isDetectingLongPress = false
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }
        
        return Circle()
            .fill(isDetectingLongPress ? Color.yellow : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(press)
    }
}

https://developer.apple.com/documentation/swiftui/adding-interactivity-with-gestures#Update-transient-UI-state

GestureState

A property wrapper type that updates a property while the user performs a gesture and resets the property back to its initial state when the gesture ends.

ユーザーがジェスチャを実行している間にプロパティを更新し、ジェスチャが終了するとプロパティを初期状態にリセットするプロパティ ラッパー タイプ。

@propertyWrapper @frozen
struct GestureState<Value>

SwiftUI gives us a specific property wrapper for tracking the state of gestures, helpfully called @GestureState. Although you can accomplish the same using a simple @State property wrapper, @GestureState comes with the added ability that it automatically sets your property back to its initial value when the gesture ends, and it’s usually significantly faster than using a simple @State as well.

SwiftUI は、@GestureState と呼ばれる、ジェスチャーの状態を追跡するための特定のプロパティ ラッパーを提供します。 単純な @State プロパティ ラッパーを使用しても同じことを実現できますが、@GestureState には、ジェスチャの終了時にプロパティを自動的に初期値に戻す追加機能が付属しており、通常は単純な @State を使用するよりも大幅に高速です。

@Stateに書き直したバージョンがありました。
ジェスチャ終了時に初期値へ戻るようになっています。

struct  ContentView : View { 
    @State  var isPressed =  false 
    var body: some  View { 
        VStack { 
            Rectangle () 
                .fill(.orange).frame(width: 200 , height: 200 ) 
                .gesture( DragGesture (minimumDistance: 0 ).onChanged { _  in
                     isPressed =  true
                 }.onEnded{ _  in
                     isPressed =  false
                 }) 
                .overlay( 
                    Text (isPressed ?  "Pressing" : "" ) 
                ) 
        } 
    } 
}

真似したい参考資料

真似して作ってみたい‥🎨
メモとして残しておきます!

まとめ

応用的な動きができるよう理解を深めていきたいですー!
資料を作成してくださる先輩方、ありがとうございます🙇

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1