iOS开发如何通过长按结束触摸手势实现UIView精准放置
问题需求
- 实现可拖拽的小型UIView控件,支持精准放置到屏幕任意位置
- 常规手指抬起放置的方案存在抬指瞬间的微小位移,无法满足精度要求
- 预期交互:拖动控件到目标位置后,停顿指定毫秒数即可自动锁定控件位置,无需抬指,锁定后控件不再跟随手指移动,直到下一次拖拽开始
已尝试方案及存在问题
基于touchesBegan/Moved/Ended生命周期的实现
存在问题:手动调用touchesEnded或touchesCancelled方法无法真正终止系统维护的触摸事件流,触发自定义的结束逻辑后,控件依然会跟随手指移动,不符合需求。
对应实现代码:
import UIKit class ViewController: UIViewController { let greenDot : UIView = { let greenDot = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) greenDot.backgroundColor = .green greenDot.layer.cornerRadius = greenDot.bounds.height / 2 return greenDot }() var timer : Timer? var lastTouch = Set<UITouch>() var lastTouchEvent: UIEvent? override func viewDidLoad() { super.viewDidLoad() } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } print(touch.location(in: view)) greenDot.center = touch.location(in: view) view.addSubview(greenDot) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } print(touch.location(in: view)) if timer != nil { timer?.invalidate() } greenDot.center = touch.location(in: view) view.addSubview(greenDot) lastTouch = touches lastTouchEvent = event timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(stopTouches), userInfo: nil, repeats: false) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } timer?.invalidate() print("Touches Ended @: \(touch.location(in: view))") } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { print("Cancelling touch") } @objc func stopTouches() { touchesEnded(lastTouch, with: lastTouchEvent) // touchesCancelled(lastTouch, with: lastTouchEvent) // view.resignFirstResponder() } }
基于UILongPressGestureRecognizer的实现
存在问题:不清楚如何在停顿达到指定时长后主动终止手势,不确定是否需要叠加两个长按手势分别处理拖动开始和结束逻辑;已知长按属于连续手势,天然适配拖拽平移场景,倾向于基于手势实现该功能。
对应实现代码:
import UIKit class ViewController: UIViewController { let greenDot : UIView = { let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) dot.backgroundColor = .green dot.layer.cornerRadius = dot.frame.width/2 return dot }() override func viewDidLoad() { super.viewDidLoad() let longPress1 = UILongPressGestureRecognizer(target: self, action: #selector(press1)) longPress1.minimumPressDuration = 0.2 view.addGestureRecognizer(longPress1) } @objc func press1(_ sender: UILongPressGestureRecognizer) { let newLocation = sender.location(in: view) greenDot.backgroundColor = .green greenDot.frame = CGRect(x: 0, y: 0, width: 10, height: 10) greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40) if sender.state == .ended { greenDot.frame = CGRect(x: 0, y: 0, width: 20, height: 20) greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40) greenDot.backgroundColor = .purple } view.addSubview(greenDot) } }
可行实现方案
核心逻辑:不要尝试手动干预系统的触摸事件生命周期,系统维护的触摸流不会因为开发者手动调用touchesEnded等方法就终止,只需要通过自定义状态标记控制控件的跟随逻辑即可,实现成本低、稳定性高。
方案基于单个UILongPressGestureRecognizer实现,配合停顿计时器和放置状态标记,具体逻辑:
- 设置极短的长按触发时长(0.1s左右),避免拖动触发延迟,同时屏蔽误触
- 增加
isPlaced布尔标记,记录控件是否已经完成放置,放置后不再响应位置更新 - 每次手势位置更新时重置停顿计时器,手指移动时不会触发放置
- 计时器达到设定的停顿时长(可自定义,比如300ms)时,将
isPlaced标记设为true,锁定控件当前位置,给出视觉/触觉反馈 - 下次点击已放置的控件时,重置
isPlaced标记,开启新一轮拖拽
可直接运行的实现代码:
import UIKit class ViewController: UIViewController { // 可拖拽的圆点控件 private let greenDot: UIView = { let dot = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) dot.backgroundColor = .systemGreen dot.layer.cornerRadius = 10 return dot }() // 停顿放置计时器 private var dwellTimer: Timer? // 控件放置状态标记 private var isPlaced = false // 停顿放置的等待时长,单位:秒,0.3即300毫秒 private let dwellWaitDuration: TimeInterval = 0.3 // 记录上一次成功放置的位置,用于抬指回退 private var lastPlacedPosition: CGPoint = .zero override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white // 配置拖拽长按手势 let dragGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleDrag(_:))) dragGesture.minimumPressDuration = 0.1 dragGesture.allowableMovement = .greatestFiniteMagnitude view.addGestureRecognizer(dragGesture) // 初始位置 greenDot.center = view.center lastPlacedPosition = view.center view.addSubview(greenDot) } @objc private func handleDrag(_ gesture: UILongPressGestureRecognizer) { let currentPos = gesture.location(in: view) switch gesture.state { case .began: // 只有点在控件范围内才触发拖动,避免全屏误触 let touchInDot = gesture.location(in: greenDot) guard greenDot.bounds.contains(touchInDot) else { // 点在其他位置直接重置手势,不响应 gesture.isEnabled = false gesture.isEnabled = true return } // 重置放置状态 isPlaced = false greenDot.backgroundColor = .systemGreen resetDwellTimer(at: currentPos) case .changed: guard !isPlaced else { return } // 更新控件位置 greenDot.center = currentPos // 移动即重置停顿计时器 resetDwellTimer(at: currentPos) case .ended, .cancelled, .failed: dwellTimer?.invalidate() if !isPlaced { // 未达到停顿放置时长就抬指,回退到上一次成功放置的位置,彻底避免抬指位移 greenDot.center = lastPlacedPosition } default: break } } // 重置停顿计时器 private func resetDwellTimer(at position: CGPoint) { dwellTimer?.invalidate() dwellTimer = Timer.scheduledTimer(withTimeInterval: dwellWaitDuration, repeats: false) { [weak self] _ in guard let self = self else { return } // 停顿达到要求时长,锁定控件 self.isPlaced = true self.lastPlacedPosition = position // 放置成功视觉反馈 self.greenDot.backgroundColor = .systemPurple // 可选触觉反馈 UIImpactFeedbackGenerator(style: .medium).impactOccurred() } } }
方案优势:
- 完全不干预系统触摸事件流,不存在手动调用touch方法不生效的问题
- 停顿触发时机完全可控,移动即重置计时,停顿时长可根据精度需求自由调整
- 锁定位置时手指仍与屏幕接触,完全避免抬指瞬间的微小位移,放置精度完全符合要求
- 仅需单个手势即可实现全流程交互,无需叠加多个手势或复杂的事件处理逻辑
内容的提问来源于stack exchange,提问作者Onymacris
相关产品推荐
相关产品推荐

