拖动包含UIButton的UIView异常:从按钮起始拖动无响应
这个问题我太熟悉了!本质是UIButton作为UIControl子类,会优先拦截触摸事件,当你从按钮上开始触摸时,按钮会立刻接管事件,导致父视图拿不到拖动的机会。要实现类似UIScrollView的delaysContentTouches效果,核心思路就是:先拦截触摸事件,延迟判断用户是要拖动还是点击按钮,再决定事件的流向。
下面给你一个完整的可落地方案,直接就能用:
方案:自定义可拖动视图,实现触摸延迟判断
我们可以自定义一个DraggableView,让它自己处理触摸事件,通过定时器延迟传递事件给按钮——如果用户在延迟期间拖动了视图,就取消按钮的事件;如果没有拖动,就让按钮响应点击。
完整代码实现
import UIKit class DraggableView: UIView { // 记录触摸开始时的初始位置 private var initialTouchPoint: CGPoint = .zero // 延迟触发按钮事件的定时器 private var touchDelayTimer: Timer? // 最小拖动阈值(超过这个距离就判定为拖动操作) private let minimumDragThreshold: CGFloat = 10.0 override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } // 初始化视图和内部按钮 private func setupUI() { isUserInteractionEnabled = true backgroundColor = .orange // 添加内部按钮 let actionButton = UIButton(type: .system) actionButton.setTitle("点击我", for: .normal) actionButton.frame = CGRect(x: 50, y: 50, width: 100, height: 50) actionButton.backgroundColor = .lightGray actionButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) addSubview(actionButton) } // 按钮点击回调 @objc private func buttonTapped() { print("按钮被正常点击啦!") } // MARK: - 触摸事件处理 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) guard let touch = touches.first else { return } initialTouchPoint = touch.location(in: self) // 启动150ms延迟定时器(和UIScrollView默认延迟一致) touchDelayTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { [weak self] _ in self?.forwardTouchToButton(touches: touches, event: event) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesMoved(touches, with: event) guard let touch = touches.first else { return } let currentTouchPoint = touch.location(in: self) // 计算触摸移动的距离 let deltaX = abs(currentTouchPoint.x - initialTouchPoint.x) let deltaY = abs(currentTouchPoint.y - initialTouchPoint.y) // 如果移动距离超过阈值,判定为拖动,取消按钮事件并移动视图 if deltaX > minimumDragThreshold || deltaY > minimumDragThreshold { touchDelayTimer?.invalidate() touchDelayTimer = nil // 更新视图位置 let newCenterX = center.x + (currentTouchPoint.x - initialTouchPoint.x) let newCenterY = center.y + (currentTouchPoint.y - initialTouchPoint.y) center = CGPoint(x: newCenterX, y: newCenterY) } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) // 如果定时器还在运行,说明没有拖动,转发事件给按钮 if let timer = touchDelayTimer, timer.isValid { timer.invalidate() forwardTouchToButton(touches: touches, event: event) } touchDelayTimer = nil } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesCancelled(touches, with: event) touchDelayTimer?.invalidate() touchDelayTimer = nil } // MARK: - 辅助方法:转发触摸事件给按钮 private func forwardTouchToButton(touches: Set<UITouch>, event: UIEvent?) { guard let touch = touches.first else { return } let touchPoint = touch.location(in: self) // 遍历子视图找到按钮(如果有多个按钮,可以根据tag/类型精准查找) for subview in subviews { guard let button = subview as? UIButton else { continue } if button.point(inside: touchPoint, with: event) { // 手动触发按钮的触摸开始和结束事件 button.touchesBegan(touches, with: event) button.touchesEnded(touches, with: event) break } } } }
在ViewController中使用
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let draggableView = DraggableView(frame: CGRect(x: 100, y: 100, width: 200, height: 200)) view.addSubview(draggableView) } }
方案原理说明
- 触摸拦截:当触摸开始时,父视图
DraggableView先接管事件,不直接传给按钮。 - 延迟判断:启动150ms定时器(和UIScrollView默认延迟一致),这段时间用来观察用户是否会拖动视图。
- 拖动判定:如果在延迟期间用户拖动距离超过阈值,就取消定时器,直接处理视图移动,不再给按钮传递事件。
- 点击转发:如果延迟结束后没有拖动,就把触摸事件手动转发给按钮,让它响应点击。
这样既解决了按钮抢占拖动事件的问题,又完整保留了按钮的点击功能,完全符合你的需求!
内容的提问来源于stack exchange,提问作者szooky
相关产品推荐
相关产品推荐

