4、滑动手势:UISwipeGestureRecognizer 方向控制与实战
滑动手势,说白了就是用户用手指在屏幕上快速划一下。这个动作太常见了——刷朋友圈、切页面、删邮件,背后都是它。我刚开始做iOS开发时,总觉得滑动手势很简单,不就是识别个方向嘛。直到有一次做相册应用,用户反馈说「为什么我向上滑总是触发删除?」我才意识到,方向控制这件事,坑比想象中多。
4.1 UISwipeGestureRecognizer 基础
UISwipeGestureRecognizer 是 UIKit 自带的手势识别器。它不像平移手势那样持续跟踪手指位置,而是等手指离开屏幕后,判断是否是一次有效的滑动。
它的核心属性就两个:
- direction:滑动方向,支持 .left、.right、.up、.down
- numberOfTouchesRequired:需要几根手指滑动,默认是1
创建方式很简单:
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
swipeGesture.direction = .right
view.addGestureRecognizer(swipeGesture)
嗯,这里要注意:一个 UISwipeGestureRecognizer 只能识别一个方向。如果你想同时支持左滑和右滑,得创建两个实例。我见过不少新手试图用 .direction = [.left, .right] 来偷懒,结果发现只有第一个方向生效。
4.2 方向控制的正确姿势
我在项目中遇到过这样一个场景:一个卡片列表,左滑显示「删除」按钮,右滑返回上一页。当时我写了两个手势:
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleLeftSwipe(_:)))
leftSwipe.direction = .left
cardView.addGestureRecognizer(leftSwipe)
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleRightSwipe(_:)))
rightSwipe.direction = .right
cardView.addGestureRecognizer(rightSwipe)
看起来没问题对吧?但测试时发现,有时候左滑会触发右滑的回调。为什么?
原因在于手势识别的「模糊匹配」。当用户手指快速划过屏幕时,系统可能无法精确判断方向,尤其是斜着滑动时。我当时的解决方案是:在手势回调里再加一层方向校验。
@objc func handleLeftSwipe(_ gesture: UISwipeGestureRecognizer) {
// 二次确认方向
guard gesture.direction == .left else { return }
// 执行左滑逻辑
showDeleteButton()
}
@objc func handleRightSwipe(_ gesture: UISwipeGestureRecognizer) {
guard gesture.direction == .right else { return }
// 执行右滑逻辑
goBack()
}
你想想看,这就像给门上了两道锁。虽然系统已经判断过一次方向,但我们在回调里再确认一次,能有效避免误触。
4.3 滑动手势与滚动视图的冲突
这是实战中最头疼的问题。你的页面里有个 UITableView 或 UICollectionView,同时你又想在上面加滑动手势。结果你会发现:手势被滚动视图「吃掉」了。
我曾经做过一个待办事项 App,每个 cell 左滑可以标记完成。但用户一滑动列表,手势就失效了。排查了半天,发现是 UISwipeGestureRecognizer 和 UIPanGestureRecognizer 冲突了。
解决方案有两种:
| 方案 | 适用场景 | 实现方式 |
|---|---|---|
| 设置代理 | 需要同时支持滑动和滚动 | 实现 gestureRecognizerShouldBegin 方法 |
| 调整优先级 | 滑动优先于滚动 | 设置 requireGestureRecognizerToFail |
我个人习惯用代理方案,更灵活:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe))
swipe.direction = .left
swipe.delegate = self
tableView.addGestureRecognizer(swipe)
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// 只在水平滑动时触发手势
if let pan = gestureRecognizer as? UIPanGestureRecognizer {
let velocity = pan.velocity(in: tableView)
return abs(velocity.x) > abs(velocity.y)
}
return true
}
}
这段代码的核心逻辑是:只有当水平速度大于垂直速度时,才让滑动手势生效。这样用户上下滚动列表时,手势不会干扰;左右滑动时,手势正常触发。
4.4 实战:实现一个可滑动的卡片堆叠效果
好了,理论说完了,咱们来点实际的。我最近在做一个社交 App,首页是卡片堆叠效果——类似探探那种,左滑不喜欢,右滑喜欢。
核心思路是:
- 用 UISwipeGestureRecognizer 识别左右滑动
- 滑动时添加动画,让卡片飞出去
- 卡片飞走后,加载下一张
代码实现:
class CardStackView: UIView {
private var currentCard: CardView?
func setupGestures() {
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeCard(_:)))
leftSwipe.direction = .left
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeCard(_:)))
rightSwipe.direction = .right
currentCard?.addGestureRecognizer(leftSwipe)
currentCard?.addGestureRecognizer(rightSwipe)
}
@objc func swipeCard(_ gesture: UISwipeGestureRecognizer) {
guard let card = currentCard else { return }
let translationX: CGFloat = gesture.direction == .left ? -300 : 300
let rotationAngle: CGFloat = gesture.direction == .left ? -0.3 : 0.3
UIView.animate(withDuration: 0.3, animations: {
card.transform = CGAffineTransform(translationX: translationX, y: 0)
.rotated(by: rotationAngle)
card.alpha = 0
}) { _ in
card.removeFromSuperview()
self.loadNextCard()
}
}
}
这里有个细节:动画的旋转角度。我试过不加旋转,卡片直直地飞出去,效果很生硬。加上一点旋转(0.3弧度大概17度),看起来就像被手指「推」出去一样自然。
4.5 避坑指南
做滑动手势开发这几年,我踩过不少坑。分享几个最常见的:
- 手势不触发:检查一下 view 的 userInteractionEnabled 是不是 true。我经常忘记在自定义 view 里开启交互。
- 方向识别错误:尤其是斜着滑动时,系统可能判断为水平或垂直。建议在手势回调里加一个角度阈值判断。
- 与系统手势冲突:比如 UINavigationController 的右滑返回。如果你在页面里也用了右滑手势,记得设置代理并处理冲突。
我曾经在一个项目里,因为没处理好手势冲突,导致用户从屏幕左侧右滑时,既触发了返回上一页,又触发了页面内的菜单。那段时间用户差评如潮……后来我加了个判断:如果滑动起点在屏幕边缘,优先触发系统手势。
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
// 屏幕左侧50pt内,交给系统处理
if location.x < 50 {
return false
}
return true
}
嗯,这个经验是用血泪换来的。你想想看,用户从屏幕边缘右滑,本能地就是想返回上一页。这时候你弹出个菜单,用户不骂你骂谁?
4.6 小结
滑动手势看着简单,但要做好真不容易。方向控制、冲突处理、动画配合,每个环节都有讲究。我个人觉得,滑动手势的精髓不在于「识别」,而在于「配合」——配合用户的预期,配合页面的交互,配合系统的行为。
下一章我们会聊捏合手势和旋转手势,这两个手势在图片查看器里特别常用。到时候我会分享一个我做图片编辑器的实战案例,保证干货满满。