iOS技术问题:重现UIActionSheet/UIAlertController拖拽高亮及命中检测原理
实现iOS UIActionSheet/UIAlertController拖拽高亮效果 + 解析其命中检测机制
嘿,这个问题我刚好研究过,下面分两部分给你拆解清楚:
一、实现拖拽高亮效果(以UIAlertController为例)
毕竟UIActionSheet在iOS 8之后就被UIAlertController取代了,所以咱们基于UIAlertController来实现类似的拖拽高亮效果,核心思路就是监听触摸事件,实时判断手指落在哪个action按钮上,然后切换高亮状态:
自定义UIAlertController子类
先建一个子类,比如HighlightableAlertController,专门用来处理触摸事件:class HighlightableAlertController: UIAlertController { private var currentHighlightedAction: UIAlertAction? override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesMoved(touches, with: event) guard let touch = touches.first, let alertView = view.subviews.first else { return } // 把触摸点转换成alertView坐标系下的位置 let touchPoint = touch.location(in: alertView) // 挨个检查每个action按钮,看触摸点是不是在它范围内 for subview in alertView.subviews { if let actionButton = subview as? UIButton, actionButton.frame.contains(touchPoint) { // 找到这个按钮对应的UIAlertAction let action = actionForButton(actionButton) if action != currentHighlightedAction { // 取消上一个按钮的高亮 currentHighlightedAction?.setValue(false, forKey: "highlighted") // 给当前按钮设置高亮 action?.setValue(true, forKey: "highlighted") currentHighlightedAction = action } return } } // 如果触摸不在任何按钮上,就取消所有高亮 currentHighlightedAction?.setValue(false, forKey: "highlighted") currentHighlightedAction = nil } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) // 手指抬起后,把高亮状态重置 currentHighlightedAction?.setValue(false, forKey: "highlighted") currentHighlightedAction = nil } // 通过按钮找到对应的UIAlertAction private func actionForButton(_ button: UIButton) -> UIAlertAction? { // 这里用了UIAlertController的私有属性关联,如果怕审核风险,可以改成给按钮加tag,和action的下标对应 guard let actions = value(forKey: "actions") as? [UIAlertAction], let buttonIndex = button.value(forKey: "index") as? Int, buttonIndex < actions.count else { return nil } return actions[buttonIndex] } }小提示:如果担心私有属性过不了审核,你可以在添加action的时候,给对应的按钮设置tag(比如第n个action对应tag n),然后通过tag来匹配对应的action。
调用自定义的AlertController
用法和普通UIAlertController完全一样:let alert = HighlightableAlertController(title: "测试拖拽高亮", message: "滑动手指试试切换高亮", preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "选项一", style: .default, handler: { _ in print("选了选项一") })) alert.addAction(UIAlertAction(title: "选项二", style: .default, handler: { _ in print("选了选项二") })) alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: { _ in })) present(alert, animated: true)
二、UIActionSheet的命中检测工作机制
UIActionSheet的这套检测逻辑其实是基于UIKit的触摸事件处理链来实现的,核心步骤大概是这样:
- 触摸事件的捕获:当用户手指按下时,触摸事件会从UIApplication一路传递到UIActionSheet的视图层级,UIActionSheet先判断触摸点是不是在自己的视图范围内,是才继续处理。
- 实时位置判断:在
touchesMoved事件触发时,UIActionSheet会持续获取触摸点的坐标,然后遍历所有选项按钮的frame,判断触摸点落在哪个按钮的范围内。 - 滚动状态识别:当选项过多需要滚动时,UIActionSheet内部其实封装了一个UIScrollView。它会监听scrollView的
isDragging和isDecelerating状态——如果scrollView处于滚动状态,就暂时禁用拖拽高亮逻辑,避免用户滚动时误触发高亮。 - 高亮状态切换:一旦检测到触摸点从一个按钮移到另一个按钮,会立刻取消前一个按钮的高亮,给当前按钮加上高亮;如果触摸移出所有按钮的范围,就把所有高亮都取消。
- 触摸结束的处理:当手指抬起时,UIActionSheet会检查当前高亮的按钮,触发对应的点击事件,然后重置所有按钮的高亮状态。
另外提一句:UIAlertController的actionSheet样式其实就是沿用了UIActionSheet的这套检测逻辑,只是内部实现更封装了而已。
内容的提问来源于stack exchange,提问作者evenwerk
相关产品推荐
相关产品推荐

