SwiftUI에서 제스처 작업하기
제스처(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 객체
- 제스처에 해당하는 애니메이션의 현재 상태
- DragGesture.Value 인스턴스
[ 드래그 제스처 인식기 + 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 뷰가 원래 위치로 돌아감
제스처 구성하기
여러 개의 제스처 결합
- 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) } }
롱 프레스 제스처와 드래그 제스처를 동시에 구성
- 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) } }
롱 프레스 제스처가 완료된 후에 드래그 작업 시작
- exclusively 수정자 (배타적으로)