创新方案
在 SwiftUI 中实现、审查或修复手势交互时(点击…
用现代 SwiftUI 手势 API 正确处理手势识别、组合、状态管理与冲突解决。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 SwiftUI 中实现、审查或修复手通用
解决思路
- 根据手势类型选择正确 API:TapGesture(离散,Void)、LongPressGesture(离散,Bool)、DragGesture(连续,DragGesture.Value)、MagnifyGesture(连续,iOS 17+)、RotateGesture(连续,iOS 17+)、SpatialTapGesture(离散,iOS 16+);离散手势用 .onEnded,连续手势用 .onChanged/.onEnded/.updating。
- TapGesture:用 count 参数支持多击,如 TapGesture(count: 2).onEnded { handleDoubleTap() },或用简写 Text("Tap me").onTapGesture(count: 2) { handleDoubleTap() }。
- LongPressGesture:用 minimumDuration 设置按住时长、maximumDistance 设置移动容差,如 LongPressGesture(minimumDuration: 1.0, maximumDistance: 10).onEnded { _ in triggerHaptic() };也可用简写 .onLongPressGesture(minimumDuration:perform:onPressingChanged:)。
- DragGesture:从 Value 读取 startLocation、location、translation、velocity、predictedEndTranslation;用 minimumDistance 和 coordinateSpace 配置,如 DragGesture(minimumDistance: 20, coordinateSpace: .global);注意 DragGesture.Value.velocity 自 iOS 13+ 可用,不要与 iOS 17+ 的 MagnifyGesture/RotateGesture 混淆。
- MagnifyGesture(iOS 17+)替代已废弃的 MagnificationGesture:用 .updating($magnifyBy) { value, state, _ in state = value.magnification } 配合 .scaleEffect(magnifyBy) 实现捏合缩放。
- RotateGesture(iOS 17+)替代 RotationGesture:用 RotateGesture(minimumAngleDelta: .degrees(1)).onChanged { value in angle = value.rotation } 配合 .rotationEffect(angle) 实现双指旋转。
- 手势组合:用 .simultaneously(with:) 让两个手势同时识别(值为 SimultaneousGesture.Value,含 .first/.second 可选值);用 .sequenced(before:) 让第一个成功后第二个才开始(如 LongPressGesture(minimumDuration: 0.5).sequenced(before: DragGesture()),用 guard case .second(true, let drag?) = value 解包);用 .exclusively(before:) 只让一个成功且第一个优先(用 switch result { case .first(_): ... case .second(_): ... } 处理)。
- 用 @GestureState 管理瞬时状态:手势结束时自动重置为初始值,适合临时反馈;持久值用 @State。例如 @GestureState private var dragOffset = CGSize.zero 与 @State private var position = CGSize.zero 配合,在 .updating 中更新 dragOffset,在 .onEnded 中把 translation 累加到 position。可用 @GestureState(resetTransaction: Transaction(animation: .spring)) 自定义重置动画。
- 用 .updating() 在连续手势中提供中间视觉反馈,例如长按时改变颜色/缩放:LongPressGesture(minimumDuration: 0.8).updating($isPressing) { current, state, _ in state = current }。
- 处理父子手势优先级:.gesture() 优先级低于视图或子视图已有手势;.highPriorityGesture() 让新增手势优先;.simultaneousGesture() 让新增手势与已有手势同优先级同时处理。按需显式选择关系,不要假设父级 .gesture() 会覆盖子手势。
- 用 GestureMask 控制参与的手势:.gesture(drag, including: .gesture) 只启用新增手势并禁用子视图手势;.subviews 只启用子视图手势;.all 为默认(两者都启用);.none 全部禁用。
- 自定义 Gesture:实现 Gesture 协议,定义 Value 类型并提供 body(如用 DragGesture().map { ... } 映射到自定义值),用 AnyGesture<Value> 包装;再通过 View 扩展提供便捷 API,如 extension View { func onSwipe(perform action: @escaping (SwipeGesture.Direction) -> Void) -> some View { gesture(SwipeGesture().onEnded(action)) } }。
- 避免在 onChanged 中做重计算:onChanged 每帧(约 60-120 Hz)调用,只做轻量状态更新(如 dragPosition = value.location),把昂贵操作(命中测试、复杂过滤)放到 onEnded 中执行一次。
- 对需要无障碍支持的操作使用 Button 而非 onTapGesture:Button 自带无障碍特征、VoiceOver 角色、Voice Control 定位、Switch Control 扫描和键盘激活;自定义外观用 ButtonStyle。仅在需要多击(count > 1)、依赖点击位置或坐标空间、或为非交互内容添加点击识别且已有合适无障碍特征时使用 onTapGesture。
- 在 onEnded 中对持久化的缩放/旋转值做合理范围钳制(clamp)。
- 手势驱动的动画使用 .spring 或类似曲线以获得自然的减速效果。
- 审查时逐项检查:手势类型是否正确(用 MagnifyGesture/RotateGesture 而非已废弃变体)、@GestureState 与 @State 使用是否正确、是否用 .updating() 提供中间反馈、父子冲突是否用 .highPriorityGesture()/.simultaneousGesture() 解决、onChanged 是否轻量、组合器是否选对、持久值是否钳制、自定义 Gesture 是否返回 body 并用 AnyGesture<Value>、动画是否用 .spring、是否考虑 GestureMask、onTapGesture 是否仅用于 count > 1/位置/坐标空间场景。
- 纠正 Apple API 可用性、废弃或行为声明时,在回复中引用相关 Sosumi 或 Apple 官方文档 URL。
适用边界 · 注意事项
- 需要完整拖拽重排、捏合缩放、旋转+缩放组合、速度/投影、序列手势状态机或手势相关 UIKit 互操作示例时,应加载 references/gesture-patterns.md 参考文件。
- 涉及更广泛的 SwiftUI 架构/状态归属时属于 swiftui-patterns;列表、滚动、表单、控件布局属于 swiftui-layout-components;广泛的 UIKit 桥接属于 swiftui-uikit-interop。