之前兩篇筆記都是講的如何在一個(gè)App內(nèi)創(chuàng)建及使用Drag&Drop, 在自己的App中集成這些新特性可以豐富交互效果, 起碼不用自己寫一套拖拽了。
但是如果能在不同App之間建立起Drag&Drop,事情就會(huì)更方便了, 運(yùn)用好的話整體的操作體驗(yàn)就會(huì)上一個(gè)Level。
如圖是演示demo的效果:

主要分兩部分來(lái)介紹怎么實(shí)現(xiàn)拖拽來(lái)傳輸數(shù)據(jù)吧。
Drag
第一部分是將自己App內(nèi)的對(duì)象Drag 出去。
其實(shí)只要添加了DragInteraction的UIView對(duì)象都可以在UI上拖到別的App內(nèi),但是如果沒有指定拖拽的數(shù)據(jù)類型或者數(shù)據(jù)加載的方式,那別的App也是無(wú)法正常讀出你正在拖拽的數(shù)據(jù)信息的。
Step 0 給UITableView 添加drag手勢(shì)
在前兩篇筆記中我們提到想要讓視圖對(duì)象可以拖拽就需要添加一個(gè)UIDragInteraction,但是在針對(duì)TableView或者CollectionView這樣的表格視圖時(shí)就不用那么麻煩了。蘋果已經(jīng)為我們提供了
UITableViewDragDelegate,
UITableViewDropDelegate,
UICollectionViewDragDelegate
UICollectionViewDropDelegate。
這四個(gè)protocol中基本都包含了之前介紹過的UIDragInteractionDelegate,UIDropInteractionDelegate的交互方法,并針對(duì)TableView和CollectionView做了優(yōu)化,用起來(lái)就像實(shí)現(xiàn)它們的DataSource一樣簡(jiǎn)單。
為了實(shí)現(xiàn)拖拽,你需要給每個(gè)支持拖拽的cell返回一個(gè)UIDragItem,實(shí)現(xiàn):
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
實(shí)現(xiàn)上述代理后,雖然發(fā)現(xiàn)tableView中的Cell可以拖動(dòng)了。
Setp1 注冊(cè)數(shù)據(jù)類型及加載器
但是怎么才能讓別的App知道我拖動(dòng)的是什么類型的數(shù)據(jù)或者別人該如何獲得我的數(shù)據(jù)呢?
對(duì)于簡(jiǎn)單的Image或者Text數(shù)據(jù),建議使用之前提到過的:
let itemProvider = NSItemProvider.init(object: dragImage!)
let dragItem = UIDragItem.init(itemProvider: itemProvider)
直接傳入需要傳輸?shù)臄?shù)據(jù)即可,但是類型還是需要遵守協(xié)議NSItemProviderWriting&NSItemProviderReading
簡(jiǎn)單來(lái)說(shuō)NSItemProviderWriting 是數(shù)據(jù)提供方需要實(shí)現(xiàn)的協(xié)議,NSItemProviderReading是數(shù)據(jù)接收方需要實(shí)現(xiàn)的協(xié)議。
但是對(duì)于很多數(shù)據(jù),比如PDF,Video甚至直接一些二進(jìn)制數(shù)據(jù)我們是無(wú)法用對(duì)象來(lái)傳輸數(shù)據(jù)的,這時(shí)候就要用到NSItemProvider 的另一系列Register方法, register的方法有很多:
//注冊(cè)一個(gè)以data為基礎(chǔ)的數(shù)據(jù)loader
registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:)
//注冊(cè)一個(gè)以文件為基礎(chǔ)的數(shù)據(jù)loader
(如果目標(biāo)App需要使用文件系統(tǒng)來(lái)訪問數(shù)據(jù),可以使用以下方法,返回一個(gè)文件的NSURL)
registerFileRepresentation(forTypeIdentifier:fileOptions:visibility:loadHandler:)
//下面兩方法類似,參數(shù)不同,都是注冊(cè)一個(gè)遵守NSItemProviderProtocol的對(duì)象到ItemProvider中。
registerObject(_:visibility:)
registerObject(ofClass:visibility:loadHandler:)
//注冊(cè)比較Custom的對(duì)象,只有當(dāng)目標(biāo)App能接受的TypeIdentifier和自己所傳遞的ItemProvider注冊(cè)的TypeIdentifier一致時(shí)會(huì)調(diào)用到參數(shù)中的loadHandler,開發(fā)者應(yīng)該在這個(gè)handler中加載好數(shù)據(jù)并轉(zhuǎn)換成TypeIdentifier相應(yīng)的格式,最后調(diào)用completion,不管是成功還是失敗,因?yàn)槟繕?biāo)App需要這個(gè)狀態(tài)。
registerItem(forTypeIdentifier:loadHandler:)
關(guān)于Type Identifer
以上很多方法都有Type Identifer,其實(shí)就是文件的格式,蘋果提供的UTI Types來(lái)表示文件的格式,比如MP4在UTI Type中就是“public.mpeg-4”,詳情可參見這張表:
System-Declared Uniform Type Identifiers
這里選擇最后一個(gè)RegisterItem方法來(lái)傳輸我們的MP4文件數(shù)據(jù)
示例代碼如下:
//MARK: - UITableViewDragDelegate
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
//初始化過程中的item沒那么重要,后面沒有使用到過,但是typeIdentifier一定要和想要傳輸?shù)母袷揭恢?,所以這里是UTI標(biāo)準(zhǔn)的
let itemProvider = NSItemProvider.init(item: videoPlayURLs[indexPath.row] as NSSecureCoding, typeIdentifier: "public.mpeg-4")
itemProvider.registerItem(forTypeIdentifier: "public.mpeg-4") { (loader, data, option) in
let request = URLRequest.init(url: self.videoPlayURLs[indexPath.row] as URL)
let task = URLSession.shared.downloadTask(with: request, completionHandler: { (url, response, error) in
let videoData = NSData.init(contentsOf: url!)
//這里加載URL 的數(shù)據(jù),并以NSData的形式callback給目標(biāo)App進(jìn)行處理。
loader(videoData,nil)
})
task.resume()
}
let dragItem = UIDragItem.init(itemProvider: itemProvider)
return [dragItem]
}
畫了一張流程圖,希望可以看得更明白一點(diǎn)。

Step2 預(yù)覽圖
這個(gè)時(shí)候雖然已經(jīng)可以將cell中的視頻拖到別的App中,比如iMessage。但是美中不足的是拖動(dòng)時(shí)候懸浮的那個(gè)View可能會(huì)比較難看,因?yàn)槟J(rèn)是將整個(gè)cell的contentView提出來(lái)作為Drag時(shí)候的預(yù)覽圖的,可以通過
func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters?
來(lái)實(shí)現(xiàn)自定義的預(yù)覽
UIDragPreviewParameters
該類是專門設(shè)計(jì)用來(lái)調(diào)整Drag item的預(yù)覽圖的, 可以通過其中的屬性定義自己想要的自定義視圖
直接上Code
示例代碼:
func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters? {
let targetCell = tableView.cellForRow(at: indexPath) as! DragToTransportCell
let dragPreviewParam = UIDragPreviewParameters.init()
//這里使用cell的imageView的frame作為UIDragPreviewParameters的visiblePath
dragPreviewParam.visiblePath = UIBezierPath.init(rect: (targetCell.videoPlayerLayer?.frame)!)
return dragPreviewParam
}
實(shí)現(xiàn)以上方法后,應(yīng)該就可以發(fā)現(xiàn)預(yù)覽圖已經(jīng)變了。
以上就是對(duì)于第一部分Drag 的實(shí)現(xiàn)
Drop
第二部分就是將別的App的數(shù)據(jù)Drop到自己的App中。其實(shí)與第一部分大同小異,運(yùn)用好NSItemProvider 就可以讓你的數(shù)據(jù)傳輸在不同App之間暢通無(wú)阻。
Step0 給UITableView 添加drop手勢(shì)
實(shí)現(xiàn)tableView 的dropDelegate 即可:
主要是兩個(gè)方法:
//告訴TabieView 能否handle某個(gè)dropSession
tableView(_:canHandle:)
//所要執(zhí)行的drop動(dòng)作
tableView(_:performDropWith:)
同時(shí)也建議實(shí)現(xiàn)
//告訴tableView該怎樣處理這個(gè)dropSession(Copy or cancel or forbidden)
tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
Step1 通過NSItemProvider 獲取數(shù)據(jù)
當(dāng)然其中最重要的還是通過NSItemProvider來(lái)獲取到我們想要的數(shù)據(jù)。之前在Drag的部分我們已經(jīng)介紹過如何注冊(cè)文件和數(shù)據(jù)加載器。
與之相對(duì)的,NSItemProvider也有一系列方法來(lái)幫助我們獲得已經(jīng)注冊(cè)過的數(shù)據(jù):
loadItem(forTypeIdentifier:options:completionHandler:)
loadDataRepresentation(forTypeIdentifier:completionHandler:)
//加載一個(gè)文件(會(huì)將目標(biāo)文件拷貝到一個(gè)臨時(shí)的地方進(jìn)行讀?。?loadFileRepresentation(forTypeIdentifier:completionHandler:)
//加載一個(gè)文件(原地讀取文件)
loadInPlaceFileRepresentation(forTypeIdentifier:completionHandler:)
loadObject(ofClass:completionHandler:)
會(huì)發(fā)現(xiàn)以上load方法與register方法基本上是一一對(duì)應(yīng)的。應(yīng)該比較好理解,Drag一方來(lái)注冊(cè),Drop一方來(lái)使用。
這里我們使用與前面對(duì)應(yīng)的loadDataRepresentation 方法來(lái)進(jìn)行加載:
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let dropItem = coordinator.items[0]
let itemProvider = dropItem.dragItem.itemProvider
itemProvider.loadDataRepresentation(forTypeIdentifier: itemProvider.registeredTypeIdentifiers.first!) { (data, error) in
let tempPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, .userDomainMask, true).first
var videoPath = NSString.init(string: tempPath!)
videoPath = videoPath.appendingPathComponent("test.mp4") as NSString
let videoData = data! as NSData
let tempUrl = URL.init(fileURLWithPath: videoPath as String)
//直接獲取到視頻文件data,然后寫入到我們的文件中為后續(xù)的讀取做準(zhǔn)備
videoData.write(to: tempUrl, atomically: true)
let playerItem = AVPlayerItem.init(url: tempUrl)
let player = AVPlayer.init(playerItem: playerItem)
self.videoPlayers.append(player)
self.videoPlayURLs.append(tempUrl as NSURL)
DispatchQueue.main.async {
tableView.reloadData()
}
}
}
=========================
demo地址:https://github.com/madao1237/DragAndDropResearc
有問題共同交流學(xué)習(xí),謝謝。