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

用拖拽手势识别器替代mouseDown,适配触控输入

基于现代AppKit API实现跨应用图片拖拽(支持鼠标与触控)

WWDC26核心建议

  • 避免重写mouseDown:或自定义事件追踪循环
  • 简单双击逻辑(如if ([event clickCount] == 2))可直接用NSClickGestureRecognizer替代
  • 区分单击、拖拽、长按这类复杂交互,可参考WebKit使用NSPressGestureRecognizer的实现思路

macOS 27新增拖拽API

仅适配macOS 27及以上版本的拖拽启动API:

- (nullable NSDraggingSession *)beginDraggingSessionWithItems:(NSArray<NSDraggingItem *> *)items gesture:(NSGestureRecognizer *)gesture source:(id<NSDraggingSource>)source NS_SWIFT_NAME(beginDraggingSession(items:gesture:source:)) API_AVAILABLE(macos(27.0));

问题:如何用现代简洁方式在NSView中实现跨应用图片拖拽?

要求同时支持鼠标与触控输入,是否有更新代码或官方指南参考?

原有mouseDown实现代码

override func mouseDown(with event: NSEvent) {
    let location = convert(event.locationInWindow, from: nil)
    let hitPoint = convert(location, to: superview)
    let hitView = super.hitTest(hitPoint)
    for textField in textFields {
        textField.isSelected = (hitView == textField)
    }
    
    let eventMask: NSEvent.EventTypeMask = [.leftMouseUp, .leftMouseDragged]
    let timeout = NSEvent.foreverDuration
    selectedTextField = hitView as? TextField
    if let textField = selectedTextField {
        // 拖拽文本框
        let textFrame = textField.frame
        dragOriginOffset = CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)
        
        if event.clickCount == 2 {
            textField.isSelected = false
            window?.makeFirstResponder(textField)
        } else {
            window?.trackEvents(matching: eventMask, timeout: timeout, mode: .eventTracking, handler: { (event, stop) in
                guard let event = event else { return }

                if event.type == .leftMouseUp {
                    stop.pointee = true
                }
                
                let movedLocation = convert(event.locationInWindow, from: nil)
                let movedOrigin = CGPoint(x: movedLocation.x - dragOriginOffset.x, y: movedLocation.y - dragOriginOffset.y)
                textField.frame = constrainRectCenterToBounds(CGRect(origin: movedOrigin, size: textFrame.size))
            })
        }
    } else if image != nil {
        // 拖拽扁平化图片
        window?.trackEvents(matching: eventMask, timeout: timeout, mode: .eventTracking, handler: { (event, stop) in
            guard let event = event else { return }
            
            if event.type == .leftMouseUp {
                stop.pointee = true
            } else {
                let movedLocation = convert(event.locationInWindow, from: nil)
                if abs(movedLocation.x - location.x) > dragThreshold || abs(movedLocation.y - location.y) > dragThreshold {
                    stop.pointee = true
                    if let delegate = delegate {
                        let draggingItem = NSDraggingItem(pasteboardWriter: delegate.pasteboardWriter(forImageCanvas: self))
                        draggingItem.setDraggingFrame(overlay.frame, contents: draggingImage)
                        beginDraggingSession(with: [draggingItem], event: event, source: self)
                    }
                }
            }
        })
    }
}

现代实现方案

1. 用手势识别器替代自定义事件追踪

抛弃mouseDown:重写,改用NSPressGestureRecognizer处理拖拽触发,NSClickGestureRecognizer处理双击:

步骤1:添加手势识别器

在视图初始化或viewDidLoad中配置手势:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 双击手势(处理文本框激活)
    let doubleClickGesture = NSClickGestureRecognizer(target: self, action: #selector(handleDoubleClick(_:)))
    doubleClickGesture.numberOfClicksRequired = 2
    addGestureRecognizer(doubleClickGesture)
    
    // 按压/拖拽手势(处理文本框移动与图片拖拽)
    let pressGesture = NSPressGestureRecognizer(target: self, action: #selector(handlePress(_:)))
    pressGesture.minimumPressDuration = 0 // 立即响应,适配拖拽与单击区分
    pressGesture.delegate = self
    addGestureRecognizer(pressGesture)
}

步骤2:实现手势回调

@objc private func handleDoubleClick(_ gesture: NSClickGestureRecognizer) {
    let location = gesture.location(in: self)
    let hitPoint = convert(location, to: superview)
    if let textField = super.hitTest(hitPoint) as? TextField {
        textField.isSelected = false
        window?.makeFirstResponder(textField)
    }
}

@objc private func handlePress(_ gesture: NSPressGestureRecognizer) {
    let location = gesture.location(in: self)
    let hitPoint = convert(location, to: superview)
    let hitView = super.hitTest(hitPoint)
    
    // 更新文本框选中状态
    textFields.forEach { $0.isSelected = ($0 == hitView) }
    
    switch gesture.state {
    case .began:
        selectedTextField = hitView as? TextField
        if let textField = selectedTextField {
            dragOriginOffset = CGPoint(x: location.x - textField.frame.minX, y: location.y - textField.frame.minY)
        }
    case .changed:
        if gesture.distance > dragThreshold {
            // 触发拖拽逻辑
            if let textField = selectedTextField {
                // 文本框拖拽移动
                let movedLocation = gesture.location(in: self)
                let movedOrigin = CGPoint(x: movedLocation.x - dragOriginOffset.x, y: movedLocation.y - dragOriginOffset.y)
                textField.frame = constrainRectCenterToBounds(CGRect(origin: movedOrigin, size: textField.frame.size))
            } else if let image = image, let delegate = delegate {
                // 图片跨应用拖拽
                let draggingItem = NSDraggingItem(pasteboardWriter: delegate.pasteboardWriter(forImageCanvas: self))
                draggingItem.setDraggingFrame(overlay.frame, contents: draggingImage)
                
                if #available(macOS 27.0, *) {
                    beginDraggingSession(with: [draggingItem], gesture: gesture, source: self)
                } else {
                    // 兼容旧系统,使用原有API
                    beginDraggingSession(with: [draggingItem], event: gesture.event!, source: self)
                }
                gesture.state = .ended // 结束手势,避免重复触发
            }
        }
    case .ended, .cancelled:
        selectedTextField = nil
    default:
        break
    }
}

// 确保双击手势优先触发
extension YourView: NSGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: NSGestureRecognizer) -> Bool {
        // 双击手势触发时,按压手势需失效
        return gestureRecognizer is NSPressGestureRecognizer && otherGestureRecognizer is NSClickGestureRecognizer
    }
}

2. 遵循官方拖拽规范

实现跨应用拖拽需注意:

  • 正确实现NSDraggingSource协议方法(如draggingSession:sourceOperationMaskForDraggingContext:)
  • 针对大图片场景,使用文件承诺优化拖拽性能,遵循官方“通过文件承诺支持拖拽”的设计规范

关键优势

  • 自动适配鼠标、触控板、触摸屏输入,无需额外处理
  • 抛弃自定义事件循环,代码结构更简洁易维护
  • 手势识别器自带状态管理,避免手动处理事件流转的复杂逻辑

内容的提问来源于stack exchange,提问作者Marek H

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 10:14:49