Skip to content

Latest commit

 

History

History
1640 lines (1638 loc) · 78.4 KB

Drag and Drop Tutorial for macOS.md

File metadata and controls

1640 lines (1638 loc) · 78.4 KB

macOS Drag和Drop的教程

Drag_and_Drop_Tutorial_for_macOS

学习全部关于macOS拖拽(drag)和投放(drop)的东西!

自从Mac发明开始,拖拽和投放就是用户交互的一部分。典型的(quintessential)例子就是Finder,你可以拖拽文件来进行排列组织,或将它们投放到垃圾桶中。

有趣的东西不至于此。

你可以从相册拖拽你最近的日落全景到你的Message,或从Dock上的Downloads中将一个文件拖拽到邮箱中。你已经get到这个点了对么?它非常得酷,并且是macOS体验的不可分割的一部分(an integral part)。

拖拽和投放从它开始已经走过了一条很长的路,现在你已几乎可以拖拽任何东西到任何地方。尝试一下,你会高兴地惊奇于这个动作和你最喜欢的app支持的类型。

在这个macOS的拖拽和投放的教程中,你将了解到怎样添加支持到你自己的app中,这样用户就可以在你的app中获得完整的Mac的体验。

这一路,你将学到怎样去:

  • NSView       的子类中实现核心的拖拽及投放动作        
  •       接受从其它应用丢过来的数据        
  • 提供将要拖拽到你app中其它view的数据
  • 创建定制的拖拽类型

开始啦!

这个项目最低需要Swift 3和Xcode 8 beta 6的环境。下载 起始的项目 ,在Xcode中打开,并build和执行它。

window-starting

遇见这个项目App

很多孩子喜欢玩贴纸,并使用它们制成很酷的拼图,因此你将构建一个app来实现这个体验。你可以将图片拖拽到一个表面上,然后你通过添加星星(sparkle)和独角兽(unicorn)到这个view上,来提高它的档次(kick things up a few notches)。

毕竟,怎么可能会不喜欢星星和独角兽?:]

保持你的集中注意力在目标上 - 构建拖拽和投放的支持 - 起始的项目已完成了你需要的view。全部你需要做的就是了解拖拽和投放的机制。

在项目窗口中有三个部分:

  • 贴纸view:你将拖拽和投放其它东西的地方
  • 你将转变成两个不同的拖拽资源的小view

来看一眼项目吧。

project-display
在这个教程中,你会编辑四个指定的文件,它们在两个位置上: Dragging Destinations Dragging Sources

Dragging Destinations 中:

  • StickerBoardViewController.swift :主view controller
  • DestinationView.swift :在window顶部的那个view - 它将成为你拖拽动作的容器

view-hierachy

Dragging Sources 中:

  • ImageSourceView.swift :在底部带有独角兽的图片的view,你要将它转为拖拽的资源
  • AppActionSourceView.swift :带有 星星 label的view - 你要将它转为另一种类型的拖拽的资源

这里有一些其它的文件在Drawing和Other Stuff组下,提供了一些助手方法,它们对于这个项目app是非常重要的,但是你不需要为它们花费任何时间。如果想要了解这个事是怎样构建的话,继续探索吧!

粘贴板和拖拽session

拖拽和投放包含一个 源(source) 和一个 目的地(destination)

你从一个source拖拽出一个项目,它需要实现 NSDraggingSource 协议。然后投放它到一个destination中,它则必须实现 NSDraggingDestination 协议,为了确定是接受还是拒绝收到的项目。 NSPasteboard 是用来帮助交换数据的类。

整个的过程被称作 dragging session

dragging session macroscopic view

当你用你的鼠标拖拽一样物事的时候,例如一个文件,就会发生下列的事:

  1. 当你开始拖拽的时候,一个 拖拽session 就开始了。
  2. 选择一些数据 - 通常一个图片和URL - 来表示放置在拖拽粘贴板上的信息。
  3. 你将图片投放到一个destination上,他会选择拒绝还是接受它,并采取一些动作 - 例如,移动文件到另一个目录下。
  4. 拖拽session 结束。

这就是它的要点(gist)。这是一个相当简单的概念!

第一件事,是为了从Finder和其它app接收图片,创建一个拖拽的destination。

创建一个拖拽destination

拖拽的destination是一个view或window,它接受来自拖拽粘贴板的数据类型。你要通过遵守(adopt) NSDraggingDestination 协议来创建拖拽的目的地。

这个图表从拖拽destination的角度,展示了对于拖拽session的剖析。
dragging session

创建destination包含以下几个步骤:

  1. 当构建view的时候,你必须声明从任何的拖拽session中可以接受的类型。
  2. 当一个拖拽的图片进入这个view的时候,你需要去实现决定这个view是否接受这种数据类型的逻辑,并让拖拽session知道这个决定。
  3. 当拖拽的图片“着陆”(lands on)在这个view上时,你会使用从拖拽粘贴板而来的数据,去展示它在你的view上。

是时候来撸一些代码了!

在项目的navigator中选择 DestinationView.swift 并找到方法:

func setup() {
}

将其替换为:

var acceptableTypes: Set<String> { return [NSURLPboardType] }
 
func setup() {
  register(forDraggedTypes: Array(acceptableTypes))
}

这个代码定义了支持类型的集合。在这个case中,仅支持 URLs 。然后,调用 register(forDraggedTypes:) 来接受包含这些类型的拖拽。

添加下列的代码到 DestinationView 中来分析拖拽session的数据:

//1.
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
 
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  var canAccept = false
 
  //2.
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  //3.
  if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
    canAccept = true
  }
  return canAccept
 
}

你在这里做了几件事:

  1. 创建一个字典来定义期望的URL类型(图片)。
  2. 从拖拽session信息中获取对拖拽粘贴板的引用。
  3. 询问粘贴板它是否包含任何的URL,以及这些URL是指向图片的。如果有图片的话,就接受这个拖拽。否则,拒绝它。

NSDraggingInfo 是一个协议,声明了提供有关拖拽session的信息的方法。你不会创建它们,也不会在事件之间储存它们。系统会在拖拽session期间创建必要的对象。

当这个app接收到图片时,你可以使用这个信息去提供反馈给拖拽session。

NSView 遵守 NSDraggingDestination 协议,因此你需要在 DestinationView 的实现中添加下列代码去覆盖 draggingEntered(\_:) 方法:

//1.
var isReceivingDrag = false {
  didSet {
    needsDisplay = true
  }
}
 
//2.
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
  let allow = shouldAllowDrag(sender)
  isReceivingDrag = allow
  return allow ? .copy : NSDragOperation()
}

上面的代码做了这些事:

  1. 创建了一个名为 isReceivingDrag 的property,以便去追踪当前有拖拽session在这个view中,并含有你想要的数据。每次设置时,都会触发这个view的重绘。
  2. 覆盖 draggingEntered(\_:) 方法,并确定它是否接受这个拖拽操作。

在第二部分,这个方法需要返回一个 NSDragOperation 。你有可能注意到鼠标指针的变化是依赖于你按住的键或拖拽的destination的。

例如,如果你在 Finder 的拖拽期间按住 Option 键,那个指针就会获得一个绿色的 + 符号,用来展示一个文件的拷贝即将发生。这个值是你如何控制那些指针的变化。

在这个配置中,如果拖拽粘贴板带有一个图片,它就返回 .copy 来向用户展示你将要复制图片。否则,如果它不接受拖拽的项目,它就返回 NSDragOperation()

处理退出

进入view的东西同时也有可能退出,所以app需要处理当一个拖拽session没有投放,就退出了你的view时的情况。添加下列的代码:

override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}

你已经覆盖了 draggingExited(\_:) 方法,并设置 isReceivingDrag 变量为 false

告诉用户正在发生什么

你几乎已经完成了第一段的代码!用户喜欢当一些事在背后发生时,能够看到一个视觉上的提示,所以,接下来,你要添加一小段绘图的代码,来保持你的用户在体验闭环(loop)上。

仍然是在 DestinationView.swift 中,找到 draw(:_) 并用下列代码替换它。

override func draw(_ dirtyRect: NSRect) {
 
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
 
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}

当一个有效的拖拽进入到这个view时,这个代码就会绘制出一个系统颜色的代码。除了看起来很尖锐,它通过当接受一个拖拽的项目时,提供视觉的表现,使你的app与系统其余部分保持一致。

注意: 想要了解更多关于自定义绘图的内容?查看我们的 macOS教程:Core Graphics

build并执行,然后尝试将一个图片从Finder拖拽到 StickerDrag 中。如果你没有顺手的图片,就使用项目目录中的 sample.jpg 吧。

buildrun-add-plus

你可以看到,当在这个view中时,指针会带有一个 + 的符号。并且view会在周围绘制一个边框。

当你退出这个view的时候,边框和 + 就会消失;只要你拖拽的不是一个图片文件,绝对任何事都不会发生。

结束拖拽

现在,到了这个部分的最后一步:你必须接受拖拽,处理数据,并告知拖拽session这已发生。

添加下列代码到 DestinationView 类的实现中:

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
  let allow = shouldAllowDrag(sender)
  return allow
}

当你在这个view中释放鼠标的时候,系统就会调用上面的方法;这是最后一次接受或拒绝拖拽的机会。返回 false 就会拒绝,导致拖拽的图像跑回了它起始的位置。返回 true 意味着view接受了这个image。当接受时,系统就会移除拖拽的图片并调用协议序列中的下一个方法: performDragOperation(\_:)

添加下列方法到 DestinationView 中:

override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  //1.
  isReceivingDrag = false
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  //2.
  let point = convert(draggingInfo.draggingLocation(), from: nil)
  //3.
  if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:filteringOptions) as? [URL], urls.count > 0 {
    delegate?.processImageURLs(urls, center: point)
    return true
  }
  return false
 
}

你做的事情有:

  1. 重置 isReceivingDrag 标致为 false
  2. 将基于window的坐标转为view的相对坐标。
  3. 将图片的URL移交给delegate进行处理,并返回 true - 否则,你拒绝拖拽操作并返回 false

注意 :感觉更好了?如果你要做一个带动画的投放序列, performDragOperation(:_) 会是开始动画最好的地方。

祝贺!你刚刚完成了第一部分,并完成了 DestinationView 接收拖拽要做的所有工作。

使用DestinationView的数据

接下来你将使用 DestinationView 在它的delegate中使用的数据。

打开 StickerBoardViewController.swift 并将其指定为 DestinationView 的delegate。

为了恰当地使用它,你需要实现 DestinationViewDelegate协议 的方法,将图片放到目标的层上。找到 processImage(\_:center:) 并用下列的代码替换它。

func processImage(_ image: NSImage, center: NSPoint) {
 
  //1.
  invitationLabel.isHidden = true
 
  //2.
  let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
 
  //3.
  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)
 
  //4.
  let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
  subview.frameCenterRotation = maxrotation
 
}

这个代码玩了下列的招(tricks):

  1. Drag Images Here label隐藏。
  2. 为投放的图片,算出其保持长宽比的情况下,最大的尺寸。
  3. 使用这个尺寸构建了一个subview,将它的中心定位在投放点上,并将其添加到view的图层上。
  4. 随机地旋转这个view一点角度,让它看起来更好。

到这里,你已经准备好了去实现处理投放到这个view的图片的URL的方法。
使用下列代码替换 processImageURLs(\_:center:) 方法:

func processImageURLs(_ urls: [URL], center: NSPoint) {
  for (index,url) in urls.enumerated() {
 
    //1.
    if let image = NSImage(contentsOf:url) {
 
      var newCenter = center
      //2.
      if index > 0 {
        newCenter = center.addRandomNoise(Appearance.randomNoise)
      }
 
      //3.
      processImage(image, center:newCenter)
    }
  }
}

你在这里做的是:

  1. 使用URL的内容中创建图片。
  2. 如果这里有超过一张的图片,就将图片的中心偏移一些,来创建分层的,随机的效果。
  3. 将图片和中心点传递到上一个方法,让它可以添加图片到view上。

现在build并执行,然后拖拽一个(或几个)图片到app的window上,投放它!

window-demo-1

看看那张图板,等待大胆的幻想~~

你大约已走了一半的路,探索过怎样使如何一个view变为拖拽的destination,以及怎样使它接受一个标准类型 - 在这个case中,是图片的URL。

Intermission: let's all go to the lobby and get ourselves some drinks. And snacks. And new iMacs

创建拖拽Source

你已经玩转了接受这一头的,但是发送这一头的呢?

在这一部分,你将学到怎样通过让那些独角兽和星星自由地活动,并在适当的环境中给用户的图像带来快乐,让你的app充满能量(supercharge your app)。

所有的拖动source都必须遵循 NSDraggingSource 协议。这个MVP(最重要的玩家)承担了将一个或多个类型的数据(或数据的“承诺”(promise))放置到拖拽板上的任务。它还提供一个拖拽图片来展示数据。

当这个图片最终着陆在它的目标时,这个destination就从粘贴板中解档数据。或者是,拖拽source就可以实现提供数据的承诺。

你需要提供两种不同类型的数据:一个标准的 Cocoa 类型(一个image)和你创建的定制类型。

提供标准的拖动类型

拖拽source将是 ImageSourceView - 包含独角兽的view的类。你的目的很简单:把这个独角兽弄到你的拼图(collage)上。

这个类需要遵循必须的协议 NSDraggingSource NSPasteboardItemDataProvider ,因此打开 ImageSourceView.swift 并添加下列的extension:

// MARK: - NSDraggingSource
extension ImageSourceView: NSDraggingSource {
  //1.
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
    return .generic
  }
}
 
// MARK: - NSDraggingSource
extension ImageSourceView: NSPasteboardItemDataProvider {
  //2.
  func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
    //TODO: Return image data
  }
}
  1. 这个方法是 NSDraggingSource 协议要求的。它告诉拖拽session你在尝试的操作类型,当用户从这个view中拖拽时。在这个case中,它是一个泛型的操作。
  2. 这实现了强制的 NSPasteboardItemDataProvider 方法。后面还有更多的东西 - 现在的只是一点点的根。

开始一个拖拽session

在真实世界的项目中,启动一个拖拽session的最佳时机取决于你的UI。

在这个项目app中,这个你工作所在的特定的view,是为了拖拽的单独的目标存在的,因此你将启动拖拽在 mouseDown(with:) 这里。

在其它case中,它启动在 mouseDragged(with:) 事件中启动可能会更恰当。

ImageSourceView 类的实现中添加下列方法:

override func mouseDown(with theEvent: NSEvent) {
  //1.
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setDataProvider(self, forTypes: [kUTTypeTIFF])
 
  //2.
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
 
  //3.
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
}

当你点击一个view时,系统调用 mouseDown(with:) ,事件开始“滚动”(rolling)。基类的实现中没有做任何事,因此无需再去调用。在实现中的代码做了全部的事:

  1. 创建一个 NSPasteboardItem ,并设定这个类作为它的数据提供者。 NSPasteboardItem 是一个“箱子”,可以“运载”有关被拖拽的项目的信息。 NSPasteboardItemDataProvider 根据请求提供数据。在这个case中你将提供 TIFF 数据,这是在Cocoa中运载图片的标准形式。
  2. 创建一个 NSDraggingItem 并将粘贴板的项目(item)赋给它。存在拖拽项目(item)来提供拖拽的图片,并运载一个粘贴板项目(item),但由于它有限的寿命,你不能保留对这个项目(item)的引用。如果需要的话,拖拽session会重新创建这个对象。 snapshot() 是前面提到的助手方法之一。它创建一个 NSView NSImage
  3. 开始拖拽session。这里你触发拖拽图片,来跟随你的鼠标,直到你投放它。

build并执行。尝试拖拽独角兽到顶部的view。

buildrun-drag-unicorn

这个view的一个图片跟随你的鼠标,但它滑到了你鼠标的后面,因为 DestinationView 不接受TIFF数据。

拿TIFF

为了接受这个数据,你需要:

  1. setup() 中更新注册的类型来接受TIFF数据
  2. 更新 shouldAllowDrag() 来接受TIFF类型
  3. 更新 performDragOperation(\_:) 来从粘贴板中拿到图片

打开 DestinationView.swift

替换下面这行:

var acceptableTypes: Set<String> { return [NSURLPboardType] }

为:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }
var acceptableTypes: Set<String> { return nonURLTypes.union([NSURLPboardType]) }

你刚刚注册了TIFF类型,就像你为URL做的,并创建一个子集来下次使用。

接下来,来到 shouldAllowDrag(:_) ,并添加发现 return canAccept 。输入下列代码在 return 语句之前:

else if let types = pasteBoard.types, nonURLTypes.intersection(types).count > 0 {
  canAccept = true
}

这里你检查了 nonURLTypes 集合是否包含了任何从粘贴板接受到的类型,如果是的话,接受拖拽的操作。从你添加了一个TIFF类型到这个集合,这个view就接受从粘贴板而来的TIFF数据。

解档图片数据

最后,更新 performDragOperation(\_:) 来解档从粘贴板来的图片数据。这相当得容易。

Cocoa想让你使用粘贴板,并提供了一个 NSImage 的带有 NSPasteboard 参数的构造方法。当你开始探索更多关于拖拽和投放的内容后,你将在 Cocoa 中发现更多的这些便利方法。

找到 performDragOperation(\_:) ,在最后添加下列的代码,就在return语句 return false 的上面:

else if let image = NSImage(pasteboard: pasteBoard) {
  delegate?.processImage(image, center: point)
  return true
}

这从粘贴板中抽取一张图片,并传递给delegate来处理。

build并执行,然后拖拽独角兽到sticker view上面。

buildrun-drag-unicorn-plus

你将注意到,现在你的指针上带有了一个绿色的 +

destination view接受图片数据,但是当你投放的时候,图片仍然滑到了后面。啊啊啊啊啊啊啊...这里缺少了什么?

向我展示图片数据!

你需要获得拖拽source来提供图片数据 - 换句话说:“履行它的承诺”。

打开 ImageSourceView.swift 并用下列代码替换 pasteboard(\_:item:provideDataForType:) 的内容:

//1.
if let pasteboard = pasteboard, type == String(kUTTypeTIFF), let image = NSImage(named:"unicorn") {
  //2.
  let finalImage = image.tintedImageWithColor(NSColor.randomColor())
  //3.
  let tiffdata = finalImage.tiffRepresentation
  pasteboard.setData(tiffdata, forType:type)
}

在这个方法中,发生了下面的事情:

  1. 如果期望的数据类型是 kUTTypeTIFF ,你就加载一个名为 unicorn 的图片。
  2. 使用提供的助手方法之一,来一任意的颜色给图片染色。毕竟,带颜色的独角兽比几个(a smattering of)全黑的独角兽更喜庆(festive)。:]
  3. 将图片转为TIFF数据,并将其放置到粘贴板上。

运行项目,拖拽独角兽的图片到sticker view上。它将投放带颜色的独角兽到view上。赞!

buildrun-add-unicorns

好多的独角兽!

拖拽定制类型

独角兽是相当棒的(fabulous),但没有星星怎么能好?奇怪的是,没有相应于星星的 Cocoa 数据类型。我打赌你已经知道接下来要做什么了。:]

sparkle

注意: 在上一部分,提供了你一个标准的数据类型。你可以在 API参考 中探索标准数据的类型。

在这一部分,你将发明你自己的数据类型。

在你的to-do列表中有下列任务:

  1. 用你定制的类型创建一个新的拖拽source。
  2. 更新拖拽destination来识别这个类型。
  3. 更新view controller来相应这个类型。

创建拖拽Source

打开 AppActionSourceView.swift 。除了这个重要的定义,它几乎是空的:

enum SparkleDrag {
  static let type = "com.razeware.StickerDrag.AppAction"
  static let action = "make sparkles"
}

这定义了你定制的拖拽类型,和动作的id。

拖拽source的types必须是 Uniform Type Identifiers 。这些是描述数据类型的反向编码的名称路径。

例如,如果你将 kUTTypeTIFF 的值打印出来,你将看到它的值就是字符串 public.tiff

为了避免和已存在的类型的冲突,你可以定义id像这样: bundle identifier + AppAction 。这是一个任意的值,但你可以保持它在应用的私有命名空间下,来最小化使用了已存在名称的危险。

如果你尝试用一个不是 UTI 的类型构建 NSPasteboardItem ,和这个操作将会失败。

现在你需要让 AppActionSourceView 遵循 NSDraggingSource 协议。打开 AppActionSourceView.swift 并添加下列的extension:

// MARK: - NSDraggingSource
extension AppActionSourceView: NSDraggingSource {
 
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor
    context: NSDraggingContext) -> NSDragOperation {
 
    switch(context) {
    case .outsideApplication:
      return NSDragOperation()
    case .withinApplication:
      return .generic
    }
  }
}

这个代码块和 ImageSourceView 不同,因为你将放置私人的数据到粘贴板上,它们在app的外部是没有意义的。这就是为什么当鼠标被拖拽到你的应用之外时,你使用 context 参数来返回 NSDragOperation()

你早已熟悉了下一步。你需要覆盖 mouseDown(with:) 事件来用一个粘贴板项目(item)启动一个拖拽session。

添加下列的代码到 AppActionSourceView 类的实现中:

override func mouseDown(with theEvent: NSEvent) {
 
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setString(SparkleDrag.action, forType: SparkleDrag.type)
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
 
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
 
}

在这里你做了什么?

你构建了一个粘贴板项目(item),并为你定制的类型,直接将数据放到它内部。在这个case中的数据是一个接收的view可以用来做决定的定制动作的id。

你可以看到这个和 ImageSourceView 在某种程度上的不同之处。投放的数据直接到了粘贴板上,而不是将数据的产生推迟到当view使用 NSPasteboardItemDataProvider 协议接受了投放时。

为什么你要用 NSPasteboardItemDataProvider 协议?因为你希望当你在 mouseDown(with:) 中开始拖拽session时,东西能够移动得尽可能得快。

如果你移动的数据花费了太长的时间在粘贴板上构建,它会阻塞(jam up)主线程,并在用户开始拖拽时,让他们感受到一个令人沮丧的可察觉的延迟。

在这个case重,你只是放置了一个小字符创到粘贴板上,以便它可以立即执行。

接受新的类型

下一步,你必须让destination view接受这个新的类型。现在,你早已知道了怎么可以做到。

打开 DestinationView.swift 并添加 SparkleDrag.type 到注册类型。替换下面这行:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }

为:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF),SparkleDrag.type] }

现在SparkleDrags就可以被接受了!

performDragOperation(:_) 需要一个新的 else-if 字句,因此添加下列的代码到这个方法的最后 return false 之前:

else if let types = pasteBoard.types, types.contains(SparkleDrag.type),
  let action = pasteBoard.string(forType: SparkleDrag.type) {
  delegate?.processAction(action, center:point)
  return true
}

这就从粘贴板中抽取了字符串。如果它符合你定制的类型,你就将动作传回给delegate。

你已几乎完成,只需要更新 StickerBoardViewController 来处理动作指令。

处理动作指令

打开 StickerBoardViewController.swift 并替换 processAction(\_:center:) 为:

func processAction(_ action: String, center: NSPoint) {
  //1.
  if action == SparkleDrag.action  {
    invitationLabel.isHidden = true
 
    //2.
    if let image = NSImage(named:"star") {
 
      //3.
      for _ in 1..<Appearance.numStars {
 
        //A.
        let maxSize:CGFloat = Appearance.maxStarSize
        let sizeChange = CGFloat(arc4random_uniform(Appearance.randonStarSizeChange))
        let finalSize = maxSize - sizeChange
        let newCenter = center.addRandomNoise(Appearance.randomNoiseStar)
 
        //B.
        let imageFrame = NSRect(x: newCenter.x, y: newCenter.y, width: finalSize , height: finalSize)
        let imageView = NSImageView(frame:imageFrame)
 
        //C.
        let newImage = image.tintedImageWithColor(NSColor.randomColor())
 
        //D.
        imageView.image = newImage
        targetLayer.addSubview(imageView)
      }
    }
  }
}

以上代码做了下列的事:

  1. 仅响应已知的动作
  2. 从bundle中载入一个星的图片
  3. 制作一些这个星的图片的拷贝,并...
    1. 生成一些随机的数字来改变星的位置。
    2. 创建一个 NSImageView 并设置它的frame。
    3. 这个图片设定一个随机的颜色 - 除非你要向David Bowie致敬(tribute),黑色的星总是有些野蛮。 (译者注:David Bowie是英国著名的摇滚音乐家,2016年去世,《Blackstar》是其遗作,在59届格莱美奖中获五项大奖)
    4. 放置图片到这个view上。

运行项目。现在你可以拖拽星星的view到sticker view上了,来添加“一阵”(a spray of)星星到你的view上。

final

从这儿去向哪里?

恭喜,你已经在你自己的app中创建了一个定制的拖放UI!

你可以使用 Save Image To Desktop 按钮来保存你的图片作为一个 JPG ,名叫 StickerDrag 。或者更进一步,将它发送给 @rwenderlich 的团队。

这里是完整项目的 源码

这个macOS的拖放教程,覆盖了Cocoa拖放机制的基础,包括:

  • 创建一个拖拽destination并接受几种不同类型的数据
  • 使用拖拽session的生命循环,来提供给用户拖拽操作的反馈
  • 从粘贴板中解码信息
  • 创建一个拖拽source,并提供推迟的数据
  • 创建一个提供指定数据类型的拖拽source

现在你已经有了用来在任意mac app中支持拖放的知识和经验了。

这里有更多要去学的。

你可以学习诸如如何在拖拽期间改变拖拽的图片,或实现一个动画的投放过渡效果,或使用承诺的文件 - Photos是一个应用程序,可将承诺的数据放在拖拽粘贴板上。

另一个有趣的话题是怎么在 NSTableView NSOutlineView 中使用拖放,它们的工作方式略有不同。可以在以下的资源中进行了解:

如果你有任何关于这个macOS的拖放教程的问题或意见,请加入下面的讨论!记住,有时候生活是拖拽的体验,但每件事遇到独角兽和星星都会变得更好。:]