swiftui-gestures
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Gestures (iOS 26+)
SwiftUI 手势(iOS 26+)
Review, write, and fix SwiftUI gesture interactions. Apply modern gesture APIs
with correct composition, state management, and conflict resolution using
Swift 6.2 patterns.
评审、编写和修复SwiftUI手势交互。使用Swift 6.2模式,通过正确的组合方式、状态管理和冲突解决方案来应用现代手势API。
Contents
目录
Gesture Overview
手势概述
| Gesture | Type | Value | Since |
|---|---|---|---|
| Discrete | | iOS 13 |
| Discrete | | iOS 13 |
| Continuous | | iOS 13 |
| Continuous | | iOS 17 |
| Continuous | | iOS 17 |
| Discrete | | iOS 16 |
Discrete gestures fire once (). Continuous gestures stream
updates (, , ).
.onEnded.onChanged.onEnded.updating| 手势 | 类型 | 值 | 起始版本 |
|---|---|---|---|
| 离散型 | | iOS 13 |
| 离散型 | | iOS 13 |
| 连续型 | | iOS 13 |
| 连续型 | | iOS 17 |
| 连续型 | | iOS 17 |
| 离散型 | | iOS 16 |
离散型手势仅触发一次()。连续型手势会持续发送更新(、、)。
.onEnded.onChanged.onEnded.updatingTapGesture
TapGesture
Recognizes one or more taps. Use the parameter for multi-tap.
countswift
// Single, double, and triple tap
TapGesture() .onEnded { tapped.toggle() }
TapGesture(count: 2) .onEnded { handleDoubleTap() }
TapGesture(count: 3) .onEnded { handleTripleTap() }
// Shorthand modifier
Text("Tap me").onTapGesture(count: 2) { handleDoubleTap() }识别一次或多次轻点。使用参数实现多点击。
countswift
// 单点、双击和三击
TapGesture() .onEnded { tapped.toggle() }
TapGesture(count: 2) .onEnded { handleDoubleTap() }
TapGesture(count: 3) .onEnded { handleTripleTap() }
// 简写修饰符
Text("Tap me").onTapGesture(count: 2) { handleDoubleTap() }LongPressGesture
LongPressGesture
Succeeds after the user holds for . Fails if finger moves
beyond .
minimumDurationmaximumDistanceswift
// Basic long press (0.5s default)
LongPressGesture()
.onEnded { _ in showMenu = true }
// Custom duration and distance tolerance
LongPressGesture(minimumDuration: 1.0, maximumDistance: 10)
.onEnded { _ in triggerHaptic() }With visual feedback via + :
@GestureState.updating()swift
@GestureState private var isPressing = false
Circle()
.fill(isPressing ? .red : .blue)
.scaleEffect(isPressing ? 1.2 : 1.0)
.gesture(
LongPressGesture(minimumDuration: 0.8)
.updating($isPressing) { current, state, _ in state = current }
.onEnded { _ in completedLongPress = true }
)Shorthand: .
.onLongPressGesture(minimumDuration:perform:onPressingChanged:)用户按住达到时长后触发成功。如果手指移动超出则触发失败。
minimumDurationmaximumDistanceswift
// 基础长按(默认0.5秒)
LongPressGesture()
.onEnded { _ in showMenu = true }
// 自定义时长和距离容差
LongPressGesture(minimumDuration: 1.0, maximumDistance: 10)
.onEnded { _ in triggerHaptic() }通过 + 提供视觉反馈:
@GestureState.updating()swift
@GestureState private var isPressing = false
Circle()
.fill(isPressing ? .red : .blue)
.scaleEffect(isPressing ? 1.2 : 1.0)
.gesture(
LongPressGesture(minimumDuration: 0.8)
.updating($isPressing) { current, state, _ in state = current }
.onEnded { _ in completedLongPress = true }
)简写方式:。
.onLongPressGesture(minimumDuration:perform:onPressingChanged:)DragGesture
DragGesture
Tracks finger movement. provides , ,
, , and .
ValuestartLocationlocationtranslationvelocitypredictedEndTranslationswift
@State private var offset = CGSize.zero
RoundedRectangle(cornerRadius: 16)
.fill(.blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in offset = value.translation }
.onEnded { _ in withAnimation(.spring) { offset = .zero } }
)Configure minimum distance and coordinate space:
swift
DragGesture(minimumDistance: 20, coordinateSpace: .global)追踪手指移动。包含、、、和。
ValuestartLocationlocationtranslationvelocitypredictedEndTranslationswift
@State private var offset = CGSize.zero
RoundedRectangle(cornerRadius: 16)
.fill(.blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in offset = value.translation }
.onEnded { _ in withAnimation(.spring) { offset = .zero } }
)配置最小距离和坐标空间:
swift
DragGesture(minimumDistance: 20, coordinateSpace: .global)MagnifyGesture (iOS 17+)
MagnifyGesture(iOS 17+)
Replaces the deprecated . Tracks pinch-to-zoom scale.
MagnificationGestureswift
@GestureState private var magnifyBy = 1.0
Image("photo")
.resizable().scaledToFit()
.scaleEffect(magnifyBy)
.gesture(
MagnifyGesture()
.updating($magnifyBy) { value, state, _ in
state = value.magnification
}
)With persisted scale:
swift
@State private var currentScale = 1.0
@GestureState private var gestureScale = 1.0
Image("photo")
.scaleEffect(currentScale * gestureScale)
.gesture(
MagnifyGesture(minimumScaleDelta: 0.01)
.updating($gestureScale) { value, state, _ in state = value.magnification }
.onEnded { value in
currentScale = min(max(currentScale * value.magnification, 0.5), 5.0)
}
)替代已弃用的。追踪捏合缩放比例。
MagnificationGestureswift
@GestureState private var magnifyBy = 1.0
Image("photo")
.resizable().scaledToFit()
.scaleEffect(magnifyBy)
.gesture(
MagnifyGesture()
.updating($magnifyBy) { value, state, _ in
state = value.magnification
}
)持久化缩放比例:
swift
@State private var currentScale = 1.0
@GestureState private var gestureScale = 1.0
Image("photo")
.scaleEffect(currentScale * gestureScale)
.gesture(
MagnifyGesture(minimumScaleDelta: 0.01)
.updating($gestureScale) { value, state, _ in state = value.magnification }
.onEnded { value in
currentScale = min(max(currentScale * value.magnification, 0.5), 5.0)
}
)RotateGesture (iOS 17+)
RotateGesture(iOS 17+)
Replaces the deprecated . Tracks two-finger rotation angle.
RotationGestureswift
@State private var angle = Angle.zero
Rectangle()
.fill(.blue).frame(width: 200, height: 200)
.rotationEffect(angle)
.gesture(
RotateGesture(minimumAngleDelta: .degrees(1))
.onChanged { value in angle = value.rotation }
)With persisted rotation:
swift
@State private var currentAngle = Angle.zero
@GestureState private var gestureAngle = Angle.zero
Rectangle()
.rotationEffect(currentAngle + gestureAngle)
.gesture(
RotateGesture()
.updating($gestureAngle) { value, state, _ in state = value.rotation }
.onEnded { value in currentAngle += value.rotation }
)替代已弃用的。追踪双指旋转角度。
RotationGestureswift
@State private var angle = Angle.zero
Rectangle()
.fill(.blue).frame(width: 200, height: 200)
.rotationEffect(angle)
.gesture(
RotateGesture(minimumAngleDelta: .degrees(1))
.onChanged { value in angle = value.rotation }
)持久化旋转角度:
swift
@State private var currentAngle = Angle.zero
@GestureState private var gestureAngle = Angle.zero
Rectangle()
.rotationEffect(currentAngle + gestureAngle)
.gesture(
RotateGesture()
.updating($gestureAngle) { value, state, _ in state = value.rotation }
.onEnded { value in currentAngle += value.rotation }
)Gesture Composition
手势组合
.simultaneously(with:)
— both gestures recognized at the same time
.simultaneously(with:).simultaneously(with:)
— 同时识别两个手势
.simultaneously(with:)swift
let magnify = MagnifyGesture()
.onChanged { value in scale = value.magnification }
let rotate = RotateGesture()
.onChanged { value in angle = value.rotation }
Image("photo")
.scaleEffect(scale)
.rotationEffect(angle)
.gesture(magnify.simultaneously(with: rotate))The value is with and optionals.
SimultaneousGesture.Value.first.secondswift
let magnify = MagnifyGesture()
.onChanged { value in scale = value.magnification }
let rotate = RotateGesture()
.onChanged { value in angle = value.rotation }
Image("photo")
.scaleEffect(scale)
.rotationEffect(angle)
.gesture(magnify.simultaneously(with: rotate))返回值为,包含和两个可选属性。
SimultaneousGesture.Value.first.second.sequenced(before:)
— first must succeed before second begins
.sequenced(before:).sequenced(before:)
— 第一个手势成功后才会触发第二个
.sequenced(before:)swift
let longPressBeforeDrag = LongPressGesture(minimumDuration: 0.5)
.sequenced(before: DragGesture())
.onEnded { value in
guard case .second(true, let drag?) = value else { return }
finalOffset.width += drag.translation.width
finalOffset.height += drag.translation.height
}swift
let longPressBeforeDrag = LongPressGesture(minimumDuration: 0.5)
.sequenced(before: DragGesture())
.onEnded { value in
guard case .second(true, let drag?) = value else { return }
finalOffset.width += drag.translation.width
finalOffset.height += drag.translation.height
}.exclusively(before:)
— only one succeeds (first has priority)
.exclusively(before:).exclusively(before:)
— 仅其中一个手势触发成功(第一个拥有优先级)
.exclusively(before:)swift
let doubleTapOrLongPress = TapGesture(count: 2)
.map { ExclusiveResult.doubleTap }
.exclusively(before:
LongPressGesture()
.map { _ in ExclusiveResult.longPress }
)
.onEnded { result in
switch result {
case .first(let val): handleDoubleTap()
case .second(let val): handleLongPress()
}
}swift
let doubleTapOrLongPress = TapGesture(count: 2)
.map { ExclusiveResult.doubleTap }
.exclusively(before:
LongPressGesture()
.map { _ in ExclusiveResult.longPress }
)
.onEnded { result in
switch result {
case .first(let val): handleDoubleTap()
case .second(let val): handleLongPress()
}
}@GestureState
@GestureState
@GestureState@Stateswift
@GestureState private var dragOffset = CGSize.zero // resets to .zero
@State private var position = CGSize.zero // persists
Circle()
.offset(
x: position.width + dragOffset.width,
y: position.height + dragOffset.height
)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
)Custom reset with animation:
@GestureState(resetTransaction: Transaction(animation: .spring))@GestureState@Stateswift
@GestureState private var dragOffset = CGSize.zero // 会重置为.zero
@State private var position = CGSize.zero // 持久化保存
Circle()
.offset(
x: position.width + dragOffset.width,
y: position.height + dragOffset.height
)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
)带动画的自定义重置:
@GestureState(resetTransaction: Transaction(animation: .spring))Adding Gestures to Views
为视图添加手势
Three modifiers control gesture priority in the view hierarchy:
| Modifier | Behavior |
|---|---|
| Default priority. Child gestures win over parent. |
| Parent gesture takes precedence over child. |
| Both parent and child gestures fire. |
swift
// Problem: parent tap swallows child tap
VStack {
Button("Child") { handleChild() } // never fires
}
.gesture(TapGesture().onEnded { handleParent() })
// Fix 1: Use simultaneousGesture on parent
VStack {
Button("Child") { handleChild() }
}
.simultaneousGesture(TapGesture().onEnded { handleParent() })
// Fix 2: Give parent explicit priority
VStack {
Text("Child")
.gesture(TapGesture().onEnded { handleChild() })
}
.highPriorityGesture(TapGesture().onEnded { handleParent() })有三个修饰符用于控制视图层级中的手势优先级:
| 修饰符 | 行为 |
|---|---|
| 默认优先级。子手势优先于父手势。 |
| 父手势优先于子手势。 |
| 父手势和子手势同时触发。 |
swift
// 问题:父级轻点手势覆盖子级轻点手势
VStack {
Button("Child") { handleChild() } // 永远不会触发
}
.gesture(TapGesture().onEnded { handleParent() })
// 修复方案1:在父视图上使用.simultaneousGesture()
VStack {
Button("Child") { handleChild() }
}
.simultaneousGesture(TapGesture().onEnded { handleParent() })
// 修复方案2:为父视图设置明确优先级
VStack {
Text("Child")
.gesture(TapGesture().onEnded { handleChild() })
}
.highPriorityGesture(TapGesture().onEnded { handleParent() })GestureMask
GestureMask
Control which gestures participate when using :
.gesture(_:including:)swift
.gesture(drag, including: .gesture) // only this gesture, not subviews
.gesture(drag, including: .subviews) // only subview gestures
.gesture(drag, including: .all) // default: this + subviews使用时,可通过GestureMask控制参与的手势:
.gesture(_:including:)swift
.gesture(drag, including: .gesture) // 仅当前手势,不包含子视图手势
.gesture(drag, including: .subviews) // 仅子视图手势
.gesture(drag, including: .all) // 默认:当前手势 + 子视图手势Custom Gesture Protocol
自定义Gesture协议
Create reusable gestures by conforming to :
Gestureswift
struct SwipeGesture: Gesture {
enum Direction { case left, right, up, down }
let minimumDistance: CGFloat
let onSwipe: (Direction) -> Void
init(minimumDistance: CGFloat = 50, onSwipe: @escaping (Direction) -> Void) {
self.minimumDistance = minimumDistance
self.onSwipe = onSwipe
}
var body: some Gesture {
DragGesture(minimumDistance: minimumDistance)
.onEnded { value in
let h = value.translation.width, v = value.translation.height
if abs(h) > abs(v) {
onSwipe(h > 0 ? .right : .left)
} else {
onSwipe(v > 0 ? .down : .up)
}
}
}
}
// Usage
Rectangle().gesture(SwipeGesture { print("Swiped \($0)") })Wrap in a extension for ergonomic API:
Viewswift
extension View {
func onSwipe(perform action: @escaping (SwipeGesture.Direction) -> Void) -> some View {
gesture(SwipeGesture(onSwipe: action))
}
}通过遵循协议创建可复用手势:
Gestureswift
struct SwipeGesture: Gesture {
enum Direction { case left, right, up, down }
let minimumDistance: CGFloat
let onSwipe: (Direction) -> Void
init(minimumDistance: CGFloat = 50, onSwipe: @escaping (Direction) -> Void) {
self.minimumDistance = minimumDistance
self.onSwipe = onSwipe
}
var body: some Gesture {
DragGesture(minimumDistance: minimumDistance)
.onEnded { value in
let h = value.translation.width, v = value.translation.height
if abs(h) > abs(v) {
onSwipe(h > 0 ? .right : .left)
} else {
onSwipe(v > 0 ? .down : .up)
}
}
}
}
// 使用示例
Rectangle().gesture(SwipeGesture { print("Swiped \($0)") })将其封装为扩展以提升API易用性:
Viewswift
extension View {
func onSwipe(perform action: @escaping (SwipeGesture.Direction) -> Void) -> some View {
gesture(SwipeGesture(onSwipe: action))
}
}Common Mistakes
常见错误
1. Conflicting parent/child gestures
1. 父子手势冲突
swift
// DON'T: Parent .gesture() conflicts with child tap
VStack {
Button("Action") { doSomething() }
}
.gesture(TapGesture().onEnded { parentAction() })
// DO: Use .simultaneousGesture() or .highPriorityGesture()
VStack {
Button("Action") { doSomething() }
}
.simultaneousGesture(TapGesture().onEnded { parentAction() })swift
// 错误写法:父级.gesture()与子级轻点手势冲突
VStack {
Button("Action") { doSomething() }
}
.gesture(TapGesture().onEnded { parentAction() })
// 正确写法:使用.simultaneousGesture()或.highPriorityGesture()
VStack {
Button("Action") { doSomething() }
}
.simultaneousGesture(TapGesture().onEnded { parentAction() })2. Using @State instead of @GestureState for transient state
2. 临时状态使用@State而非@GestureState
swift
// DON'T: @State doesn't auto-reset — view stays offset after gesture ends
@State private var dragOffset = CGSize.zero
DragGesture()
.onChanged { value in dragOffset = value.translation }
.onEnded { _ in dragOffset = .zero } // manual reset required
// DO: @GestureState auto-resets when gesture ends
@GestureState private var dragOffset = CGSize.zero
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}swift
// 错误写法:@State不会自动重置 —— 手势结束后视图仍保持偏移
@State private var dragOffset = CGSize.zero
DragGesture()
.onChanged { value in dragOffset = value.translation }
.onEnded { _ in dragOffset = .zero } // 需要手动重置
// 正确写法:@GestureState在手势结束时自动重置
@GestureState private var dragOffset = CGSize.zero
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}3. Not using .updating() for intermediate feedback
3. 未使用.updating()提供中间反馈
swift
// DON'T: No visual feedback during long press
LongPressGesture(minimumDuration: 2.0)
.onEnded { _ in showResult = true }
// DO: Provide feedback while pressing
@GestureState private var isPressing = false
LongPressGesture(minimumDuration: 2.0)
.updating($isPressing) { current, state, _ in
state = current
}
.onEnded { _ in showResult = true }swift
// 错误写法:长按过程中无视觉反馈
LongPressGesture(minimumDuration: 2.0)
.onEnded { _ in showResult = true }
// 正确写法:在按压过程中提供反馈
@GestureState private var isPressing = false
LongPressGesture(minimumDuration: 2.0)
.updating($isPressing) { current, state, _ in
state = current
}
.onEnded { _ in showResult = true }4. Using deprecated gesture types on iOS 17+
4. iOS 17+仍使用已弃用的手势类型
swift
// DON'T: Deprecated since iOS 17
MagnificationGesture() // deprecated
RotationGesture() // deprecated
// DO: Use modern replacements
MagnifyGesture() // iOS 17+
RotateGesture() // iOS 17+swift
// 错误写法:iOS 17起已弃用
MagnificationGesture() // 已弃用
RotationGesture() // 已弃用
// 正确写法:使用现代替代方案
MagnifyGesture() // iOS 17+
RotateGesture() // iOS 17+5. Heavy computation in onChanged
5. 在onChanged中执行大量计算
swift
// DON'T: Expensive work called every frame (~60-120 Hz)
DragGesture()
.onChanged { value in
let result = performExpensiveHitTest(at: value.location)
let filtered = applyComplexFilter(result)
updateModel(filtered)
}
// DO: Throttle or defer expensive work
DragGesture()
.onChanged { value in
dragPosition = value.location // lightweight state update only
}
.onEnded { value in
performExpensiveHitTest(at: value.location) // once at end
}swift
// 错误写法:每帧都会调用昂贵操作(约60-120 Hz)
DragGesture()
.onChanged { value in
let result = performExpensiveHitTest(at: value.location)
let filtered = applyComplexFilter(result)
updateModel(filtered)
}
// 正确写法:节流或延迟执行昂贵操作
DragGesture()
.onChanged { value in
dragPosition = value.location // 仅执行轻量状态更新
}
.onEnded { value in
performExpensiveHitTest(at: value.location) // 仅在结束时执行一次
}Review Checklist
评审检查清单
- Correct gesture type: /
MagnifyGesture(not deprecatedRotateGesture/Magnificationvariants)Rotation - used for transient values that should reset;
@GestureStatefor persisted values@State - provides intermediate visual feedback during continuous gestures
.updating() - Parent/child conflicts resolved with or
.highPriorityGesture().simultaneousGesture() - closures are lightweight — no heavy computation every frame
onChanged - Composed gestures use correct combinator: ,
simultaneously, orsequencedexclusively - Persisted scale/rotation clamped to reasonable bounds in
onEnded - Custom conformances use
Gesture(notvar body: some Gesture)View - Gesture-driven animations use or similar for natural deceleration
.spring - considered when mixing gestures across view hierarchy levels
GestureMask
- 使用正确的手势类型:/
MagnifyGesture(而非已弃用的RotateGesture/Magnification变体)Rotation - 临时值使用(会自动重置);持久值使用
@GestureState@State - 连续手势通过提供中间视觉反馈
.updating() - 通过或
.highPriorityGesture()解决父子手势冲突.simultaneousGesture() - 闭包保持轻量 —— 避免每帧执行大量计算
onChanged - 组合手势使用正确的组合器:、
simultaneously或sequencedexclusively - 持久化的缩放/旋转比例在中限制在合理范围内
onEnded - 自定义遵循
Gesture(而非var body: some Gesture)View - 手势驱动的动画使用或类似动画实现自然减速
.spring - 在视图层级混合手势时考虑使用
GestureMask
References
参考资料
- See for drag-to-reorder, pinch-to-zoom, combined rotate+scale, velocity calculations, and SwiftUI/UIKit gesture interop.
references/gesture-patterns.md - Gesture protocol
- TapGesture
- LongPressGesture
- DragGesture
- MagnifyGesture
- RotateGesture
- GestureState
- Composing SwiftUI gestures
- Adding interactivity with gestures
- 查看获取拖拽重排序、捏合缩放、旋转+缩放组合、速度计算以及SwiftUI/UIKit手势互操作等内容。
references/gesture-patterns.md - Gesture 协议
- TapGesture
- LongPressGesture
- DragGesture
- MagnifyGesture
- RotateGesture
- GestureState
- 组合SwiftUI手势
- 通过手势添加交互性