Gesture And Gesture State In SwiftUI

Introduction

 
SwiftUI gives us a specific property wrapper for tracking the state of the gesture. When you add multiple gestures to your app's view hierarchy, you need to decide how the gestures interact with each other. You use gesture composition to define the order SwiftUI recognizes gestures.
 
There are three gesture composition types:
  • Simultaneous - A gesture that's a sequence of two gestures.
  • Sequenced - A gesture containing two gestures that can happen at the same time with neither of them preceding the other. 
  • Exclusive - A gesture that consists of two gestures where only one of them can succeed.
When you combine gesture modifiers simultaneously, SwiftUI must recognize all subgesture patterns at the same time for it to recognize the combining gesture. When you sequence gesture modifiers one after the other, SwiftUI must recognize each subgesture in order. Finally, when you combine gestures exclusively, SwiftUI recognizes the entire gesture pattern when SwiftUI only recognizes one subgesture but not the others.
 

Sequence One Gesture After Another

 
When you sequence one gesture after another, SwiftUI recognizes the first gesture before it recognizes the second. For example, to require a long press before the user can drag a view, you sequence a DragGesture after a LongPressGesture.
 

Model Sequenced Gesture States

 
To make it easier to track complicated states, use an enumeration that captures all of the states you need to configure your views. In the following example, there are three important states,
  1. No interaction(inactive)
  2. long press in progress(pressing)
  3. Dragging(Dragging) 
  1. struct DraggableCircle: View {  
  2.   
  3.     enum DragState {  
  4.         case inactive  
  5.         case pressing  
  6.         case dragging(translation: CGSize)  
  7.           
  8.         var translation: CGSize {  
  9.             switch self {  
  10.             case .inactive, .pressing:  
  11.                 return .zero  
  12.             case .dragging(let translation):  
  13.                 return translation  
  14.             }  
  15.         }  
  16.           
  17.         var isActive: Bool {  
  18.             switch self {  
  19.             case .inactive:  
  20.                 return false  
  21.             case .pressing, .dragging:  
  22.                 return true  
  23.             }  
  24.         }  
  25.           
  26.         var isDragging: Bool {  
  27.             switch self {  
  28.             case .inactive, .pressing:  
  29.                 return false  
  30.             case .dragging:  
  31.                 return true  
  32.             }  
  33.         }  
  34.     }  
  35.       
  36.     @GestureState var dragState = DragState.inactive  
  37.     @State var viewState = CGSize.zero   

Create Gesture and Update UI State 

When you sequence two gestures, the callbacks capture the state of both gestures. In the update callback, the new value uses an enumeration to represent the combination of the possible gesture states. The code below converts the underlying gesture states into the high-level DragState enumeration defined above.
  1. var body: some View {  
  2.         let minimumLongPressDuration = 0.5  
  3.         let longPressDrag = LongPressGesture(minimumDuration: minimumLongPressDuration)  
  4.             .sequenced(before: DragGesture())  
  5.             .updating($dragState) { value, state, transaction in  
  6.                 switch value {  
  7.                 // Long press begins.  
  8.                 case .first(true):  
  9.                     state = .pressing  
  10.                 // Long press confirmed, dragging may begin.  
  11.                 case .second(true, let drag):  
  12.                     state = .dragging(translation: drag?.translation ?? .zero)  
  13.                 // Dragging ended or the long press cancelled.  
  14.                 default:  
  15.                     state = .inactive  
  16.                 }  
  17.             }  
  18.             .onEnded { value in  
  19.                 guard case .second(true, let drag?) = value else { return }  
  20.                 self.viewState.width += drag.translation.width  
  21.                 self.viewState.height += drag.translation.height  
  22.             }  
When the user begins pressing the view, the drag state changes to pressing and a shadow animates under the shape. After the user finishes the long press and the drag state changes to dragging, a border appears around the shape to indicate that the user may begin moving the view.
  1.     return Circle()  
  2.         .fill(Color.blue)  
  3.         .overlay(dragState.isDragging ? Circle().stroke(Color.white, lineWidth: 2) : nil)  
  4.         .frame(width: 100, height: 100, alignment: .center)  
  5.         .offset(  
  6.             x: viewState.width + dragState.translation.width,  
  7.             y: viewState.height + dragState.translation.height  
  8.         )  
  9.         .animation(nil)  
  10.         .shadow(radius: dragState.isActive ? 8 : 0)  
  11.         .animation(.linear(duration: minimumLongPressDuration))  
  12.         .gesture(longPressDrag)  
  13. }  
Remember, one of the advantages of @GestureState is that it automatically sets the value of your property back to its initial value when the gesture ends. It means we can drag a view around all we want, and as soon as we let go it will snap back to its original position.