SwiftUI 제스처 시스템 내부
Source: Dev.to
SwiftUI 제스처는 겉보기에는 간단해 보입니다:
.onTapGesture { }
하지만 내부적으로 SwiftUI는 강력하고 계층화된 제스처 시스템을 가지고 있어 다음을 결정합니다:
- 어떤 제스처가 승리하는지
- 어떤 제스처가 실패하는지
- 어떤 제스처가 동시에 실행되는지
- 제스처가 서로 언제 취소되는지
- 제스처가 뷰 트리를 통해 어떻게 전파되는지
대부분의 제스처 버그는 개발자가 제스처 우선순위와 해결 방식을 이해하지 못해서 발생합니다.
이 글에서는 SwiftUI 제스처가 실제로 어떻게 동작하는지, 엔진 수준에서부터 자세히 살펴봅니다 — 이를 통해 신뢰할 수 있고 예측 가능한 복잡한 인터랙션을 만들 수 있습니다.
🧠 핵심 제스처 모델
SwiftUI 제스처는 다음 파이프라인을 따릅니다:
Touch input
↓
Hit‑testing
↓
Gesture recognition
↓
Gesture competition
↓
Resolution (win / fail / simultaneous)
↓
State updates
여러 제스처가 동일한 터치를 관찰할 수 있지만, 모두가 승리하는 것은 아닙니다.
🖐 1. SwiftUI의 제스처 유형
SwiftUI는 여러 기본 제스처를 제공합니다:
TapGesture(탭 제스처)LongPressGesture(길게 누르기 제스처)DragGesture(드래그 제스처)MagnificationGesture(확대 제스처)RotationGesture(회전 제스처)
이들은 값 타입이며, 뷰 트리 안에 구성됩니다.
🧩 2. 제스처는 화면이 아니라 뷰에 연결됩니다
Text("Hello")
.onTapGesture { print("Tapped") }
이 제스처는 뷰가 히트‑테스트되는 영역에서만 작동합니다.
핵심 규칙:
📌 뷰에 크기가 없거나 히트‑테스트에 투명한 경우, 해당 제스처는 작동하지 않습니다.
탭 가능한 영역을 정의하려면 content shape를 사용하세요:
.contentShape(Rectangle())
⚔️ 3. Gesture Competition: Who Wins?
여러 제스처가 동일한 터치를 감지하면 SwiftUI는 다음 순서대로 해결합니다:
- Exclusive gestures (default)
- High‑priority gestures
- Simultaneous gestures
- Parent gestures
기본적으로 가장 깊은 하위 제스처가 승리합니다.
🥇 4. highPriorityGesture
자식 제스처의 우선순위를 무시합니다.
.view
.highPriorityGesture(
TapGesture().onEnded { print("Parent tap") }
)
사용 시기:
- 부모가 터치를 가로채야 할 때.
- 자식 인터랙션이 부모 로직을 방해하지 않아야 할 때.
경고: 과도하게 사용하면 기대되는 UX가 깨질 수 있습니다.
🤝 5. simultaneousGesture
제스처를 동시에 발생시킬 수 있습니다.
.view
.simultaneousGesture(
TapGesture().onEnded { print("Also tapped") }
)
Typical uses:
- 분석
- 햅틱
- 부가 효과
- 상호작용 로깅
This does not block other gestures.
🔗 6. 제스처 구성
SwiftUI는 제스처를 명시적으로 결합할 수 있게 해줍니다.
순차 (하나씩 차례로)
LongPressGesture()
.sequenced(before: DragGesture())
동시에
TapGesture()
.simultaneously(with: LongPressGesture())
배타적으로
TapGesture()
.exclusively(before: DragGesture())
이러한 결합자는 제스처 흐름을 완전히 제어할 수 있게 합니다.
📏 7. 제스처 상태 vs. 뷰 상태
임시 제스처 값을 위해 @GestureState를 사용하세요.
@GestureState private var dragOffset = CGSize.zero
핵심 속성:
- 제스처가 끝날 때 자동으로 리셋됩니다.
- 영구적인 상태 업데이트를 트리거하지 않습니다.
- 애니메이션 구동에 최적입니다.
예시:
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
📌 가이드라인: 움직임에는 @GestureState를, 결과에는 @State를 사용하세요.
🔄 8. Gesture Lifecycle
Every gesture has phases:
.onChanged.onEnded.updating
Internally, gestures can:
- Fail
- Cancel
- Restart
This is why gestures sometimes feel “jumpy” when their identity changes.
🧱 9. 제스처 전파 및 뷰 아이덴티티
뷰가 재생성되면:
- 제스처가 재생성됩니다.
- 제스처 상태가 초기화됩니다.
- 진행 중인 제스처가 취소됩니다.
일반적인 원인:
id()변경- 조건부 뷰
- 리스트 아이덴티티 문제
- 부모 무효화
📌 안정적인 아이덴티티 = 안정적인 제스처 동작.
📜 10. ScrollView vs. Gestures
ScrollView는 높은 우선순위의 드래그 제스처를 가지고 있어, 이는 다음을 의미합니다:
- 자식 드래그 제스처가 때때로 작동하지 않을 수 있습니다.
- 커스텀 스와이프 제스처가 제대로 동작하지 않는 것처럼 느껴질 수 있습니다.
해결책:
simultaneousGesture를 사용합니다.- 제스처를 오버레이에 연결합니다.
- 스크롤을 일시적으로 비활성화합니다.
gesture(_:including:)를 사용하여 서브뷰를 포함합니다.
.gesture(drag, including: .subviews)
⚠️ 11. 일반적인 제스처 버그 (및 해결 방법)
| 증상 | 일반적인 원인 | 해결 방법 |
|---|---|---|
| 제스처가 작동하지 않음 | View has zero size | Ensure the view has a size or add a contentShape.뷰에 크기가 있도록 하거나 contentShape을 추가하세요. |
| Hit‑testing disabled | Provide a contentShape or make the view opaque to hits.contentShape을 제공하거나 뷰를 히트에 대해 불투명하게 만드세요. | |
| 제스처가 무작위로 취소됨 | View identity changed (e.g., id() changes) | Keep view identity stable. 뷰 식별자를 안정적으로 유지하세요. |
| Parent re‑rendered | Minimize unnecessary parent updates. 불필요한 부모 업데이트를 최소화하세요. | |
| Navigation transition | Use .transaction or keep gesture state outside the transitioning view..transaction을 사용하거나 전환 중인 뷰 외부에 제스처 상태를 유지하세요. | |
| 스크롤 충돌 | Competing drag gestures | Adjust priority (highPriorityGesture) or use simultaneousGesture.우선순위를 조정( highPriorityGesture)하거나 simultaneousGesture를 사용하세요. |
시스템을 이해하면 이 모든 문제를 해결할 수 있습니다.
🧠 Mental Model Cheat Sheet
- 제스처는 views 위에서 동작합니다.
- Identity가 중요합니다 – 안정적인 ID가 제스처를 안정적으로 유지합니다.
- 기본적으로 Children win합니다.
- Priority 수정자(
highPriorityGesture,simultaneousGesture)가 동작을 변경합니다. - 동시에 발생하는 제스처는 서로 don’t block합니다.
@GestureState는 ephemeral이며,@State는 지속적입니다.ScrollView는 자체 드래그 제스처를 aggressive하게 사용합니다.- 레이아웃이 hit‑testing에 영향을 줍니다.
🚀 최종 생각
SwiftUI 제스처는 마법이 아니라 결정론적인 시스템입니다.
다음 개념을 이해하면:
- Competition
- Priority
- Identity
- Propagation
신뢰할 수 있고 복잡한 상호작용을 자신 있게 구축할 수 있습니다.
ld:
- 스와이프 동작
- 맞춤 슬라이더
- 드래그로 해제
- 멀티터치 상호작용
- 고급 애니메이션
…SwiftUI와 싸우지 않고.
