在MacOS App中拖放行为通过Dragging Session来管理,我们先来看一下Dragging Session如何定义:
Dragging Session
一个完整的Dragging Session是指从源(Source)拖放到目的地(Destination)的处理过程,其中Source需要实现协议NSDraggingSource,Destination需要实现协议NSDraggingDestination协议。在整个拖放过程中,通过NSPasteboard协助实现数据的交换。
Dragging Session的流程如下:
1. 使用鼠标拖动某一项目时自动创建了一个Dragging Session;
2. 将数据信息同步到NSPasteboard;
3. 在Destination松开,目的App或View决定是否接受;
4. 结束Dragging Session;
实现 Dragging Destination
NSView本身默认遵循了NSDraggingDestination协议,因此只需要Override必要的接口即可:
override func awakeFromNib() {
setup()
}
// 可以接受哪些类型的资源
var acceptableTypes: Set<String> { return [NSURLPboardType] }
func setup() {
register(forDraggedTypes: Array(acceptableTypes))
}
如上,接口register(forDraggedTypes:)用于注册NSView支持哪些类型的粘贴板项目。
接下来实现是接受还是拒绝来自Dragging Session的项目:
// 假定只接受图片类型
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
var canAccept = false
// 访问粘贴板实例
let pasteBoard = draggingInfo.draggingPasteboard()
// 判断是否包含想要的内容
if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
canAccept = true
}
return canAccept
}
接下来通过接口draggingEntered(:)实现判定逻辑,如果符合要求就显示图片,否则返回一个空的NSDragOperation():
var isReceivingDrag = false {
didSet {
needsDisplay = true
}
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
let allow = shouldAllowDrag(sender)
isReceivingDrag = allow
return allow ? .copy : NSDragOperation()
}
同样的,我们还需要处理draggingExited(:)接口:
override func draggingExited(_ sender: NSDraggingInfo?) {
isReceivingDrag = false
}
为了能够显示拖拽的发生过程,我们根据isReceivingDrag标志位的变化决定是否显示试图边框:
override func draw(_ dirtyRect: NSRect) {
if isReceivingDrag {
NSColor.selectedControlColor.set()
let path = NSBezierPath(rect:bounds)
path.lineWidth = Appearance.lineWidth
path.stroke()
}
}
最后当用户在视图内部松开鼠标的时候就是Dragging Session结束的时候,也是最后决定是否接受数据的机会:
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
let allow = shouldAllowDrag(sender)
return allow
}
当接口返回True的时候,系统会调用接口performDragOperation(_:),我们需要在这个接口内实现真正的接收数据逻辑:
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
isReceivingDrag = false
let pasteBoard = sender.draggingPasteboard()
let point = convert(sender.draggingLocation(), from: nil)
if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions) as? [URL], urls.count > 0 {
// 这里的urls就是图片资源的地址,我们可以通过一个Delegate来实现处理逻辑
delegate?.processImageURLs(urls, center: point)
return true
}
return false
}
Delegate的处理逻辑通过视图的对应Controller实现:
@IBOutlet var topLayer: DestinationView!
@IBOutlet var targetLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
// 设置视图的Delegate
topLayer.delegate = self
}
通过Extension实现Delegate的协议接口:
extension StickerBoardViewController: DestinationViewDelegate {
func processImageURLs(_ urls: [URL], center: NSPoint) {
for (index, url) in urls.enumerated() {
if let image = NSImage(contentsOf: url) {
var newCenter = center
if index > 0 {
newCenter = center.addRandomNoise(Appearance.randomNoise)
}
processImage(image, center: newCenter)
}
}
}
func processImage(_ image: NSImage, center: NSPoint) {
// 显示接收到的图片
let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
let subview = NSImageView(frame: NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
subview.image = image
targetLayer.addSubview(subview)
let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
subview.frameCenterRotation = maxrotation
}
}