继续学习macOS开发相关内容,在Mac上的操作拖拽是比较常见的,是人机交互的重要部分,最常用的便是Finder文件操作
- 粘贴板和拖拽session
拖拽和投放包含一个 源(source) 和一个目的地(destination)
从一个source拖拽出一个项目,它需要实现 NSDraggingSource 协议。然后投放它到一个destination中,它则必须实现 NSDraggingDestination 协议,为了确定是接受还是拒绝收到的项目。 NSPasteboard 是用来帮助交换数据的类。
整个的过程被称作 dragging session :
当拖拽一个文件的时候,就会发生下列的事:
1.当开始拖拽的时候,一个 拖拽session 就开始了
2.选择一些图片数据显示在拖拽粘贴板上
3.将图片投放到一个destination上,它会选择拒绝还是接受它,并采取一些动作 - 例如,移动文件到另一个目录下
4.拖拽session 结束
- 创建一个拖拽的destination是一个view或window,它接受来自拖拽粘贴板的数据类型,需要遵守(adopt) NSDraggingDestination 协议来创建拖拽的目的地。
这个图表从拖拽destination的角度,展示了对于拖拽session的剖析。
创建destination包含以下几个步骤:
1.当构建view的时候,必须声明从任何的拖拽session中可以接受的类型。
2.当一个拖拽的图片进入这个view的时候你需要去实现决定这个view是否接受这种数据类型的逻辑,并让拖拽session知道这个决定。
3.当拖拽的图片“着陆”(lands on)在这个view上时,你会使用从拖拽粘贴板而来的数据,去展示它在你的view上。
创建一个DestinationView.swift,作为拖拽的目的地
注册view接受的数据类型,定义了支持类型的集合,仅支持 URLs,然后调用 register(forDraggedTypes:) 来接受包含这些类型的拖拽。
override func awakeFromNib() {
setup()
}
var acceptableTypes: Set<String> { return [NSURLPboardType] }
func setup() {
//接收URL的拖拽数据
register(forDraggedTypes: Array(acceptableTypes))
}
接下来分析拖拽session中的数据
//1.创建一个字典来定义期望的URL类型(图片)
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
var canAccept = false
//2.从拖拽session信息中获取对拖拽粘贴板的引用
let pasteBoard = draggingInfo.draggingPasteboard()
//3.询问粘贴板它是否包含任何的URL,以及这些URL是指向图片的。如果有图片的话,就接受这个拖拽。否则,拒绝它
if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
canAccept = true
}
return canAccept
}
NSView 遵守 NSDraggingDestination 协议,因此需要在 DestinationView.swift override draggingEntered(:) 方法: 在这个配置中,如果拖拽粘贴板带有一个图片,它就返回 .copy 来向用户展示将要复制的图片。否则,如果它不接受拖拽的项目,它就返回 NSDragOperation()
//创建 isReceivingDrag,追踪当前有拖拽session在这个view中,是否含有需要的数据,每次设置时,都会触发这个view的重绘。
var isReceivingDrag = false {
didSet {
needsDisplay = true
}
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
let allow = shouldAllowDrag(sender)
isReceivingDrag = allow
return allow ? .copy : NSDragOperation()
}
进入DestinationView的同时也有可能退出,所以app需要处理当一个拖拽session没有投放,就退出了DestinationView时的情况,添加下列的代码:
override func draggingExited(_ sender: NSDraggingInfo?) {
isReceivingDrag = false
}
当有文件拖入到输入框时,将输入框的边框颜色改变,有一个提示的作用,DestinationView.swift 中,在override func draw(_ dirtyRect: NSRect) 方法中修改
override func draw(_ dirtyRect: NSRect) {
if isReceivingDrag {
NSColor.selectedControlColor.set()
let path = NSBezierPath(rect:bounds)
path.lineWidth = Appearance.lineWidth
path.stroke()
}
}
结束拖拽 到了这个部分的最后一步,必须接受拖拽,处理数据,并告知拖拽session。 DestinationView中实现下面的协议方法
//接受拖拽,处理数据,这里是最后一次接受或拒绝拖拽的机会
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
let allow = shouldAllowDrag(sender)
return allow
}
返回 true 意味着DestinationView接受了这个image。当接受时,系统就会移除拖拽的图片并调用协议序列中的下一个方法: 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 {
//在DestinationView中自定的协议DestinationViewDelegate传送数据
delegate?.processImageURLs(urls, center: point)
return true
}
return false
}
DestinationView中声明的协议
protocol DestinationViewDelegate {
func processImageURLs(_ urls: [URL], center: NSPoint) // 图片url
func processImage(_ image: NSImage, center: NSPoint) // 图片
}
StickerBoardViewController负责是DestinationView的控制器,将StickerBoardViewController指定为 DestinationView 的delegate。
@IBOutlet var topLayer: DestinationView!
@IBOutlet var targetLayer: NSView! //显示图片的view
override func viewDidLoad() {
super.viewDidLoad()
topLayer.delegate = self
}
需要实现 DestinationViewDelegate协议 的方法,将图片放到DestinationView,实现协议方法 processImage(:center:) 和 processImageURLs(:center:)
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
}
}
现在就可以拖拽图片到目的view上了,效果如下图所示:
还有另外的两个拖拽的知识点,后面继续总结,示例的demo地址GitHub