在 Swift 中创建 3D Touch 自定义手势识别器

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

早在 3 月份,我就在 Creating Custom Gesture Recognisers in Swift 中研究了为单次触摸旋转创建自定义 手势识别器。随着新 iPhone 6s 中 3D Touch 的引入,我认为对深压进行同样的练习将是一个有趣的练习。

我的 DeepPressGestureRecognizer 是一个扩展的 UIGestureRecognizer ,它在按下超过给定阈值时调用一个动作。它的语法与任何其他手势识别器(例如长按)相同,其实现方式如下:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


该动作也与其他识别器具有相同的状态,因此当状态为 Began 时,用户的触摸力已超过阈值:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


如果代码太多,我还创建了一个协议扩展,这意味着您只需让您的类实现 DeepPressable 即可获得深度压力识别器:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


...然后在 setDeepPressAction() 中设置适当的操作:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


遗憾的是,Apple 的 Taptic Engine 没有公共 API(但是,有 Dal Rupnik 在这里讨论的 解决方法)。我的代码没有使用私有 API,而是在识别出深压时选择性地振动设备。

深压手势识别器机制

要扩展 UIGestureRecognizer,您需要添加 桥接标头 以导入 UIKit/UIGestureRecognizerSubclass.h。一旦你有了它,你就可以自由地覆盖 touchesBegan、touchesMoved 和 touchesEnded。在 DeepPressGestureRecognizer 中,这两个方法中的第一个调用 handleTouch() 来检查:

  • 如果未识别出深按但当前力高于标准化阈值,则将该触摸事件视为深触摸手势的开始。
  • 如果识别到深按并且触摸力下降到阈值以下,则将该触摸事件视为手势的结束。


handleTouch() 的代码是:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


在 touchesEnded 中,如果没有识别出深度触摸(例如,用户轻轻点击按钮或更改滑块),我将手势的状态设置为失败:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)

视觉反馈

在无法访问 iPhone 的 Taptic Engine 的情况下,我决定在识别手势时向源组件添加辐射脉冲效果。这是通过将 CAShapeLayer 添加到组件的 CALayer 并将矩形路径从与组件大小相同的路径过渡到更大的矩形路径来完成的(感谢 Jameson Quave 这篇文章对它进行了漂亮的描述 )。


为此,首先我为开始和结束状态设置了两个 CGPath 实例:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


然后创建三个基本动画来增加路径,通过将不透明度降低为零并使笔画变粗来淡出它:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


在单个 CATransaction 中,我为所有三个动画提供相同的持续时间、计时功能等属性,并设置它们运行。动画完成后,我从源组件的层中删除脉冲层:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


DeepPressable 协议扩展

我忍不住添加一个协议扩展,使任何可以添加手势识别器的类都可以深度按压。该协议本身有两种我自己的方法来设置和删除深压动作:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


这些在扩展中被赋予了默认行为:


 let button = UIButton(type: UIButtonType.System)
button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal)

stackView.addArrangedSubview(button)

let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self,
    action: "deepPressHandler:",
    threshold: 0.75)


button.addGestureRecognizer(deepPressGestureRecognizer)


综上所述

如果无法访问 Taptic Engine,这可能不是理想的交互体验,但视觉反馈可能有助于缓解这种情况。然而,希望这篇文章能够说明将 3D Touch 信息集成到自定义手势识别器中是多么容易。您可能希望使用此示例来创建连续力手势识别器,例如在绘图应用程序中。


一如既往,这个项目的源代码可以在 我的 GitHub 存储库 中找到。享受!