用拖拽手势识别器替代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
相关产品推荐
相关产品推荐

