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] 来偷懒,结果发现只有第一个方向生效。

⚠️ 重要提醒:UISwipeGestureRecognizer 的 direction 属性是位掩码,但实际运行时只取第一个设置的方向。想支持多个方向,请创建多个手势实例。

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
    }
}

这段代码的核心逻辑是:只有当水平速度大于垂直速度时,才让滑动手势生效。这样用户上下滚动列表时,手势不会干扰;左右滑动时,手势正常触发。

💡 小技巧:如果你用的是 UIScrollView,可以设置 scrollView.panGestureRecognizer 的 requireGestureRecognizerToFail 为你的滑动手势。但要注意,这样会牺牲滚动的流畅度。

4.4 实战:实现一个可滑动的卡片堆叠效果

好了,理论说完了,咱们来点实际的。我最近在做一个社交 App,首页是卡片堆叠效果——类似探探那种,左滑不喜欢,右滑喜欢。

核心思路是:

  1. 用 UISwipeGestureRecognizer 识别左右滑动
  2. 滑动时添加动画,让卡片飞出去
  3. 卡片飞走后,加载下一张

代码实现:

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 小结

滑动手势看着简单,但要做好真不容易。方向控制、冲突处理、动画配合,每个环节都有讲究。我个人觉得,滑动手势的精髓不在于「识别」,而在于「配合」——配合用户的预期,配合页面的交互,配合系统的行为。

下一章我们会聊捏合手势和旋转手势,这两个手势在图片查看器里特别常用。到时候我会分享一个我做图片编辑器的实战案例,保证干货满满。