Drag and Drop for iOS11 (Drag to transport)

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

如圖是演示demo的效果:


Drag to transport

主要分兩部分來介紹怎么實現(xiàn)拖拽來傳輸數(shù)據(jù)吧。

Drag

第一部分是將自己App內(nèi)的對象Drag 出去。

其實只要添加了DragInteraction的UIView對象都可以在UI上拖到別的App內(nèi),但是如果沒有指定拖拽的數(shù)據(jù)類型或者數(shù)據(jù)加載的方式,那別的App也是無法正常讀出你正在拖拽的數(shù)據(jù)信息的。

Step 0 給UITableView 添加drag手勢

在前兩篇筆記中我們提到想要讓視圖對象可以拖拽就需要添加一個UIDragInteraction,但是在針對TableView或者CollectionView這樣的表格視圖時就不用那么麻煩了。蘋果已經(jīng)為我們提供了
UITableViewDragDelegate,
UITableViewDropDelegate,
UICollectionViewDragDelegate
UICollectionViewDropDelegate
。

這四個protocol中基本都包含了之前介紹過的UIDragInteractionDelegate,UIDropInteractionDelegate的交互方法,并針對TableView和CollectionView做了優(yōu)化,用起來就像實現(xiàn)它們的DataSource一樣簡單。
為了實現(xiàn)拖拽,你需要給每個支持拖拽的cell返回一個UIDragItem,實現(xiàn):

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]

實現(xiàn)上述代理后,雖然發(fā)現(xiàn)tableView中的Cell可以拖動了。

Setp1 注冊數(shù)據(jù)類型及加載器

但是怎么才能讓別的App知道我拖動的是什么類型的數(shù)據(jù)或者別人該如何獲得我的數(shù)據(jù)呢?
對于簡單的Image或者Text數(shù)據(jù),建議使用之前提到過的:

            let itemProvider = NSItemProvider.init(object: dragImage!)
            let dragItem = UIDragItem.init(itemProvider: itemProvider)

直接傳入需要傳輸?shù)臄?shù)據(jù)即可,但是類型還是需要遵守協(xié)議NSItemProviderWriting&NSItemProviderReading
簡單來說NSItemProviderWriting 是數(shù)據(jù)提供方需要實現(xiàn)的協(xié)議,NSItemProviderReading是數(shù)據(jù)接收方需要實現(xiàn)的協(xié)議。

但是對于很多數(shù)據(jù),比如PDF,Video甚至直接一些二進(jìn)制數(shù)據(jù)我們是無法用對象來傳輸數(shù)據(jù)的,這時候就要用到NSItemProvider 的另一系列Register方法, register的方法有很多:

//注冊一個以data為基礎(chǔ)的數(shù)據(jù)loader
registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:)  

//注冊一個以文件為基礎(chǔ)的數(shù)據(jù)loader
(如果目標(biāo)App需要使用文件系統(tǒng)來訪問數(shù)據(jù),可以使用以下方法,返回一個文件的NSURL)
registerFileRepresentation(forTypeIdentifier:fileOptions:visibility:loadHandler:)  

//下面兩方法類似,參數(shù)不同,都是注冊一個遵守NSItemProviderProtocol的對象到ItemProvider中。
registerObject(_:visibility:)
registerObject(ofClass:visibility:loadHandler:)  

//注冊比較Custom的對象,只有當(dāng)目標(biāo)App能接受的TypeIdentifier和自己所傳遞的ItemProvider注冊的TypeIdentifier一致時會調(diào)用到參數(shù)中的loadHandler,開發(fā)者應(yīng)該在這個handler中加載好數(shù)據(jù)并轉(zhuǎn)換成TypeIdentifier相應(yīng)的格式,最后調(diào)用completion,不管是成功還是失敗,因為目標(biāo)App需要這個狀態(tài)。
registerItem(forTypeIdentifier:loadHandler:)

關(guān)于Type Identifer
以上很多方法都有Type Identifer,其實就是文件的格式,蘋果提供的UTI Types來表示文件的格式,比如MP4在UTI Type中就是“public.mpeg-4”,詳情可參見這張表:
System-Declared Uniform Type Identifiers

這里選擇最后一個RegisterItem方法來傳輸我們的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]
    }

畫了一張流程圖,希望可以看得更明白一點。


流程圖

Step2 預(yù)覽圖

這個時候雖然已經(jīng)可以將cell中的視頻拖到別的App中,比如iMessage。但是美中不足的是拖動時候懸浮的那個View可能會比較難看,因為默認(rèn)是將整個cell的contentView提出來作為Drag時候的預(yù)覽圖的,可以通過

func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters?

來實現(xiàn)自定義的預(yù)覽

UIDragPreviewParameters

該類是專門設(shè)計用來調(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
    }

實現(xiàn)以上方法后,應(yīng)該就可以發(fā)現(xiàn)預(yù)覽圖已經(jīng)變了。

以上就是對于第一部分Drag 的實現(xiàn)

Drop

第二部分就是將別的App的數(shù)據(jù)Drop到自己的App中。其實與第一部分大同小異,運用好NSItemProvider 就可以讓你的數(shù)據(jù)傳輸在不同App之間暢通無阻。

Step0 給UITableView 添加drop手勢

實現(xiàn)tableView 的dropDelegate 即可:
主要是兩個方法:

//告訴TabieView 能否handle某個dropSession
tableView(_:canHandle:)
//所要執(zhí)行的drop動作
tableView(_:performDropWith:)

同時也建議實現(xiàn)

//告訴tableView該怎樣處理這個dropSession(Copy or cancel or forbidden)
tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)

Step1 通過NSItemProvider 獲取數(shù)據(jù)

當(dāng)然其中最重要的還是通過NSItemProvider來獲取到我們想要的數(shù)據(jù)。之前在Drag的部分我們已經(jīng)介紹過如何注冊文件和數(shù)據(jù)加載器。
與之相對的,NSItemProvider也有一系列方法來幫助我們獲得已經(jīng)注冊過的數(shù)據(jù):

loadItem(forTypeIdentifier:options:completionHandler:)
loadDataRepresentation(forTypeIdentifier:completionHandler:)
//加載一個文件(會將目標(biāo)文件拷貝到一個臨時的地方進(jìn)行讀?。?loadFileRepresentation(forTypeIdentifier:completionHandler:)
//加載一個文件(原地讀取文件)
loadInPlaceFileRepresentation(forTypeIdentifier:completionHandler:)
loadObject(ofClass:completionHandler:)

會發(fā)現(xiàn)以上load方法與register方法基本上是一一對應(yīng)的。應(yīng)該比較好理解,Drag一方來注冊,Drop一方來使用。

這里我們使用與前面對應(yīng)的loadDataRepresentation 方法來進(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í),謝謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容