关于 swiftui-gestures
swiftui-gestures 是面向 SwiftUI 开发者的手势交互技能,介绍识别、组合、状态管理和冲突处理,并涵盖手势相关的无障碍替代方式。
主要功能
- 01支持点击、长按与拖动手势
- 02处理缩放与旋转手势
- 03组合并协调多个手势
- 04管理临时及持久状态
- 05调整视图层级中的手势优先级
- 06提供手势无障碍建议
适用场景
- 01实现图片的缩放与旋转
- 02构建长按后拖动交互
- 03排查父子视图手势冲突
- 04检查手势操作的无障碍体验
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-gestures安装后在 Claude Code 中输入/swiftui-gestures确认已加载。
附带文件evals/evals.jsonreferences/gesture-patterns.md
详细介绍
SwiftUI 手势(iOS 26+)
审核、编写和修复 SwiftUI 手势交互。使用 Swift 6.3 模式,通过现代手势 API 实现正确的组合、状态管理和冲突解决。
**范围边界:**本技能负责 SwiftUI 手势识别、组合、手势状态以及手势专属的无障碍替代方案。更广泛的
SwiftUI 架构和状态所有权由 swiftui-patterns 负责;列表、滚动、表单和控件布局由
swiftui-layout-components 负责;更广泛的 UIKit 桥接由 swiftui-uikit-interop 负责。
更正 Apple API 可用性、弃用或行为相关说法时,请在回复中引用相关的 Sosumi 或 Apple 官方文档 URL。
目录
- 手势概览
- TapGesture
- LongPressGesture
- DragGesture
- MagnifyGesture(iOS 17+)
- RotateGesture(iOS 17+)
- 手势组合
@GestureState- 向视图添加手势
- 自定义手势协议
- 常见错误
- 审核清单
- 参考资料
手势概览
| 手势 | 类型 | 值 | 起始版本 |
|---|---|---|---|
TapGesture | 离散 | Void | iOS 13 |
LongPressGesture | 离散 | Bool | iOS 13 |
DragGesture | 连续 | DragGesture.Value | iOS 13 |
MagnifyGesture | 连续 | MagnifyGesture.Value | iOS 17 |
RotateGesture | 连续 | RotateGesture.Value | iOS 17 |
SpatialTapGesture | 离散 | SpatialTapGesture.Value | iOS 16 |
离散手势只触发一次(.onEnded)。连续手势会持续提供更新
(.onChanged、.onEnded、.updating)。
TapGesture
识别一次或多次点击。使用 count 参数设置多击次数。
// 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() }
LongPressGesture
用户按住达到 minimumDuration 后成功。如果手指移动超过
maximumDistance,则失败。
// 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() }
通过 @GestureState 和 .updating() 提供视觉反馈:
@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
跟踪手指移动。Value 提供 startLocation、location、
translation、velocity 和 predictedEndTranslation。
iOS 13+ 的 DragGesture 已提供 DragGesture.Value.velocity;
不要将它与 MagnifyGesture 和 RotateGesture 等 iOS 17+ 手势类型混淆。
@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 } }
)
配置最小距离和坐标空间:
DragGesture(minimumDistance: 20, coordinateSpace: .global)
MagnifyGesture(iOS 17+)
替代已弃用的 MagnificationGesture。跟踪双指捏合缩放比例。
@GestureState private var magnifyBy = 1.0
Image("photo")
.resizable().scaledToFit()
.scaleEffect(magnifyBy)
.gesture(
MagnifyGesture()
.updating($magnifyBy) { value, state, _ in
state = value.magnification
}
)
RotateGesture(iOS 17+)
RotateGesture 是 RotationGesture 的较新替代方案。跟踪双指旋转角度。
@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 }
)
如需持久化、限制范围的放大示例和旋转组合示例,请加载 references/gesture-patterns.md。
手势组合
.simultaneously(with:) — 同时识别两个手势
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:) — 第一个手势必须成功,第二个手势才会开始
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:) — 只允许一个手势成功(第一个优先)
let doubleTapOrLongPress = TapGesture(count: 2)
.exclusively(before:
LongPressGesture()
)
.onEnded { result in
switch result {
case .first(_): handleDoubleTap()
case .second(_): handleLongPress()
}
}
@GestureState
@GestureState 是一个属性包装器,当手势结束时会自动重置为初始值。用于临时反馈;需要保留的值则使用 @State。
@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
}
)
使用动画自定义重置:@GestureState(resetTransaction: Transaction(animation: .spring))
为视图添加手势
三个修饰符用于控制视图层级中的手势优先级:
| 修饰符 | 行为 |
|---|---|
.gesture() | 优先级低于视图或其子视图中已定义的手势。 |
.highPriorityGesture() | 添加的手势优先于现有手势。 |
.simultaneousGesture() | 添加的手势与现有手势以相同优先级处理。 |
let parentTap = TapGesture().onEnded { handleParent() }
VStack {
Image(systemName: "star.fill")
.onTapGesture { handleChild() }
}
.simultaneousGesture(parentTap) // Both handlers run on child content.
默认情况下,使用 .gesture(parentTap) 添加优先级较低的父视图手势;如果希望新增的父视图手势优先,则使用 .highPriorityGesture(parentTap)。
GestureMask
使用 .gesture(_:including:) 时,可控制哪些手势参与处理:
.gesture(drag, including: .gesture) // added gesture; disables subview gestures
.gesture(drag, including: .subviews) // subview gestures; disables added gesture
.gesture(drag, including: .all) // default: added + subview gestures
.gesture(drag, including: .none) // disables added + subview gestures
自定义手势协议
遵循 Gesture 协议即可创建可复用的手势:
struct SwipeGesture: Gesture {
enum Direction { case left, right, up, down }
typealias Value = Direction
let minimumDistance: CGFloat
init(minimumDistance: CGFloat = 50) {
self.minimumDistance = minimumDistance
}
var body: AnyGesture<Direction> {
AnyGesture(
DragGesture(minimumDistance: minimumDistance)
.map { value in
let h = value.translation.width, v = value.translation.height
if abs(h) > abs(v) {
return h > 0 ? .right : .left
} else {
return v > 0 ? .down : .up
}
}
)
}
}
// Usage
Rectangle().gesture(SwipeGesture().onEnded { print("Swiped \($0)") })
将其封装在 View 扩展中,以便更方便地调用 API:
extension View {
func onSwipe(perform action: @escaping (SwipeGesture.Direction) -> Void) -> some View {
gesture(SwipeGesture().onEnded(action))
}
}
常见错误
1. 误解父视图/子视图手势的优先级
不要假设父视图的 .gesture() 会覆盖子视图手势。请按照为视图添加手势中的说明,明确选择两者的关系。
2. 使用 @State 代替 @GestureState 存储临时状态
对于识别结束时应重置的值,请使用 @GestureState;持久化结果则保留在 @State 中。参见 @GestureState。
3. 未使用 .updating() 提供中间反馈
// 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 }
4. 在 iOS 17+ 中使用已弃用的手势类型
// DON'T: Deprecated since iOS 17
MagnificationGesture() // deprecated — use MagnifyGesture()
// DO: Use newer gesture types
MagnifyGesture() // iOS 17+
RotateGesture() // iOS 17+ (newer alternative to RotationGesture)
5. 在 onChanged 中执行繁重计算
// 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
}
6. 对应使用 Button 的操作使用 onTapGesture
// DON'T: onTapGesture has no accessibility traits, VoiceOver role,
// Voice Control targeting, Switch Control scanning, or keyboard activation
Text("Delete")
.onTapGesture { deleteItem() }
// DO: Button provides all of these automatically
Button("Delete", role: .destructive) { deleteItem() }
// DO: For custom visuals, use ButtonStyle instead of onTapGesture
Button { toggleExpanded() } label: {
CardView()
}
.buttonStyle(.plain)
仅在以下情况下使用 onTapGesture:多次点击(count: 2+)、行为取决于点击位置,或为已有适当无障碍特征的非交互内容添加点击识别。
检查清单
- 手势类型正确:使用
MagnifyGesture/RotateGesture(而非已弃用的Magnification/Rotation变体) - 临时且应重置的值使用
@GestureState;持久化的值使用@State -
.updating()在连续手势过程中提供中间视觉反馈 - 使用
.highPriorityGesture()或.simultaneousGesture()解决父视图/子视图冲突 -
onChanged闭包保持轻量——不要在每一帧都执行繁重计算 - 组合手势使用正确的组合方式:
simultaneously、sequenced或exclusively - 在
onEnded中将持久化的缩放/旋转限制在合理范围内 - 自定义
Gesture遵循实现应返回手势主体;映射到自定义Value时使用AnyGesture<Value> - 手势驱动的动画使用
.spring或类似效果,以实现自然减速 - 在视图层级的不同层级混合手势时考虑
GestureMask - 仅在
count > 1、需要点击位置或坐标空间时使用onTapGesture——普通的单击操作应使用Button
参考资料
- 如果任务需要完整的拖动排序、捏合缩放、旋转与缩放组合、速度/投影、序列手势状态机或手势相关的 UIKit 互操作示例,请阅读 references/gesture-patterns.md。
- Gesture 协议
- TapGesture
- LongPressGesture
- DragGesture
- DragGesture.Value.velocity
- MagnifyGesture
- RotateGesture
- GestureState
- 组合 SwiftUI 手势
- 使用手势添加交互性