SwiftUI에서 제스처 작업하기

3 분 소요

제스처(gesture)

  • 터치 스크린과 사용자 간의 인터랙션을 설명하는데 사용
  • 앱 내에서 이를 감지하여 이벤트를 실행하게 하는 데 사용
  • 드래그, 탭, 더블 탭, 핀칭, 로테이션, 롱 프레스….

기본 제스처

제스처는 제스처 인식기(gesture recognizer) 를 추가하면 감지

  • gesture() 수정자 사용
  • 하나 이상의 액션 콜백 을 포함
    • 콜백은 일치하는 제스처가 뷰에서 감지될 때 실행되는 코드를 포함

[ 탭 제스처 인식기 + onEnded 콜백 ]

var body: some View {
    Imgae(systemName: "hand.point.right.fill")
        .gesture(
            TapGesture() // 제스처 인식기
                .onEnded { _ in // 콜백
                    print("Tapped")
                }
        )
    }
}

👉 일반적으로 제스처 인식기를 변수에 할당

var body: some View {
        
    let tap = TapGesture()
        .onEnded { _ in
            print("Tapped")
        }
        
    return Image(systemName:  "hand.point.right.fill")
            .gesture(tap)
}
  • 더블 탭 (탭 횟수 지정)

    let tap = TapGesture(count: 2)
      .onEnded { _ in
          print("Tapped")
      }
    

[ 롱 프레스 제스처 인식기 + onEnded 콜백 ]

var body: some View {

    let longPress = LongPressGesture()
        .onEnded { _ in
            print("Long Press")
        }
        
    return Image(systemName:  "hand.point.right.fill")
        .gesture(longPress)
    }

디폴트 시간(0.5초) 이상 터치시 감지

  • 감지하는 데 필요한 시간 조절: minimumDuration 인자 전달
  • 롱 프레스 동안 화면상의 접촉점이 뷰 밖으로 이동할 수 있는 최대 거리 지정:
    maximumDistance 인자 전달

      let longPress = LongPressGesture(minimumDuration: 10, maximumDistance: 25)
          .onEnded { _ in
              print("Long Press")
          }
    

    10초 이상 롱 프레스, 25에서 벗어나면 제스처는 취소되며 액션호출x

  • 제스처 인식기 제거

      .gesture(nil)
    

onChanged 액션 콜백

onEnded 액션: 제스처가 완료될 때 호출
onChanged 액션: 제스처가 처음 인식되었을 때 호출, 제스처가 끝날 때까지 제스처의 값이 변할 때마다 호출

[ 확대 제스처 인식기 + onChanged 콜백 ]

var body: some View {
        
    let magnificationGesture = MagnificationGesture(minimumScaleDelta: 0)
        .onEnded { _ in
            print("Gesture Ended")
        }
        
    return Image(systemName: "hand.point.right.fill")
        .resizable()
        .font(.largeTitle)
        .gesture(magnificationGesture)
        .frame(width: 100, height: 90)
}

핀칭(pinching) 동작 감지 (Option 키 + Image 뷰 드래그)
제스처가 끝난 후에만 메시지를 출력

let magnificationGesture = MagnificationGesture(minimumScaleDelta: 0)
    .onChanged( { _ in
        print("Magnifying")
    })
    .onEnded { _ in
        print("Gesture Ended")
    }

핀치 작업과 연관된 값이 변할 때마다 onChanged 액션 호출

  • 제스처에 따라 Image 뷰의 크기 조절하기

      struct ContentView: View {
            
          @State private var magnification: CGFloat = 1.0
            
          var body: some View {
                
              let magnificationGesture = MagnificationGesture(minimumScaleDelta: 0)
                  .onChanged( { value in
                      self.magnification = value 
                  // 현재 비율을 나타내는 CGFloat 값을 가진 MagnificationGesture.Value 인스턴스가 전달
                  })
                  .onEnded { _ in
                      print("Gesture Ended")
                  }
                
              return Image(systemName: "hand.point.right.fill")
                  .resizable()
                  .font(.largeTitle)
                  .scaleEffect(magnification)
                  .gesture(magnificationGesture)
                  .frame(width: 100, height: 90)
          }
      }
    

updating 콜백 액션

updating 액션

  • onChanged와 비슷, 하지만 @GestureState 프로퍼티 래퍼 사용
    • @State 프로퍼티 래퍼와 비슷, 하지만 제스처와 함께 사용
    • 제스처가 끝나면 @GestureState는 자동으로 원래 상태 값으로 리셋(임시 상태 저장)
  • updating 액션이 호출될 때마다 세가지 인자 전달
    • DragGesture.Value 인스턴스
      제스처에 대한 정보
      • location (CGPoint) - 드래그 제스처의 현재 위치
      • predictedEndLocation (CGPoint) - 드래그를 멈추게 된다면 예상되는 최종 위치
      • predictedEndTranslation (CGSize) - 드래그를 멈추게 된다면 예상되는 최종 오프셋
      • startLocation (CGPoint) - 드래그 제스처가 시작된 위치
      • time (Date) - 현재 드래그 이벤트가 발생한 타임스탬프
      • translation (CGSize) - 드래그 제스처가 시작한 위치부터 현재 위치까지의 총 오프셋
    • @GestureState 프러파티에 대한 참조체
      제스처 바인딩
    • Transaction 객체
      제스처에 해당하는 애니메이션의 현재 상태

[ 드래그 제스처 인식기 + updating 콜백 ]
@GestureState 프로퍼티를 현재의 translation 값으로 업데이트

struct ContentView: View {
    
    @GestureState private var offset: CGSize = .zero
    
    var body: some View {
        
        let drag = DragGesture()
            .updating($offset) { dragValue, state, transaction in
                state = dragValue.translation
        }
        
        return Image(systemName: "hand.point.right.fill")
            .font(.largeTitle)
            .offset(offset)
            .gesture(drag)
    }
}

Image 뷰가 화면의 드래그 제스처를 따라 움직임 -> offset() 수정자 사용
드래그가 끝나면 자동으로 offset 프로퍼티가 원래 상태로 돌아감 -> Imgae 뷰가 원래 위치로 돌아감


제스처 구성하기

여러 개의 제스처 결합

  1. simultaneously 수정자 (동시에)

     struct ContentView: View {
        
         @GestureState private var offset: CGSize = .zero
         @GestureState private var longPress: Bool = false
        
         var body: some View {
            
             let longPressAndDrag = LongPressGesture(minimumDuration: 1.0)
                 .updating($longPress) { value, state, transaction in
                     state = value
                 }
                 .simultaneously(with: DragGesture())
                 .updating($offset) { value, state, transaction in
                     state = value.second?.translation ?? .zero
                 }
            
             return Image(systemName: "hand.point.right.fill")
                 .foregroundColor(longPress ? Color.red : Color.blue)
                 .font(.largeTitle)
                 .offset(offset)
                 .gesture(longPressAndDrag)
         }
     }
    

    롱 프레스 제스처와 드래그 제스처를 동시에 구성

  2. sequenced 수정자 (순차적으로)

     struct ContentView: View {
        
         @GestureState private var offset: CGSize = .zero
         @State private var dragEnabled: Bool = false
        
         var body: some View {
            
             let longPressBeforeDrag = LongPressGesture(minimumDuration: 2.0)
                 .onEnded({ _ in
                     self.dragEnabled = true
                 })
                 .sequenced(before: DragGesture())
                 .updating($offset) { value, state, transaction in
                    
                     switch value {
                        
                         case .first(true):
                             print("Long press in progress")
                        
                         case .second(true, let drag):
                             state = drag?.translation ?? .zero
                        
                         default: break
                     }
                 }
                 .onEnded { value in
                     self.dragEnabled = false
                 }
            
             return Image(systemName: "hand.point.right.fill")
                 .foregroundColor(dragEnabled ? Color.green : Color.blue)
                 .font(.largeTitle)
                 .offset(offset)
                 .gesture(longPressBeforeDrag)   
         }
     }
    

    롱 프레스 제스처가 완료된 후에 드래그 작업 시작

  3. exclusively 수정자 (배타적으로)

카테고리:

업데이트: