You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 23:57:19