UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.08.13 星期二

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的糕韧。感興趣的參考上面幾篇文章咧叭。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用持寄、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(二)

開始

首先看下主要內(nèi)容

主要內(nèi)容:在這個(gè)drag and drop教程中源梭,您將構(gòu)建支持UICollectionViews和兩個(gè)單獨(dú)的iOS應(yīng)用程序之間的drag and drop娱俺。

下面看一下寫作環(huán)境

Swift 5, iOS 12, Xcode 10

Apple在iOS 11中引入了拖放(drag and drop)功能,允許用戶將項(xiàng)目從一個(gè)屏幕位置拖動(dòng)到另一個(gè)屏幕位置废麻。 在iPhone上荠卷,拖放僅在應(yīng)用程序內(nèi)可用,而在iPad上烛愧,它也可以跨應(yīng)用程序使用油宜。 這對(duì)于快速將Photos中的圖像添加到電子郵件中非常方便掂碱。

在本教程中,您將通過構(gòu)建CacheManager來探索拖放慎冤,這兩個(gè)應(yīng)用程序用于管理地理緩存:

CacheMaker在正在進(jìn)行和已完成項(xiàng)目的看板中組織地理藏寶疼燥。 CacheEditor允許用戶編輯從CacheMaker帶來的geocache的詳細(xì)信息。 您將通過向兩個(gè)應(yīng)用添加拖放支持來實(shí)現(xiàn)這些管理功能蚁堤。

在Xcode中打開CacheManager.xcworkspace并選擇CacheMaker作為active scheme

構(gòu)建并運(yùn)行CacheMaker醉者。 您應(yīng)該看到兩個(gè)collection views,其中第一個(gè)包含用于正在進(jìn)行的工作的地理緩存:

嘗試將geocache從正在進(jìn)行的欄拖動(dòng)到完成的欄:

在本教程的第一部分中披诗,您的目標(biāo)是實(shí)現(xiàn)這一目標(biāo)撬即。 稍后,您將解鎖將geocaches拖放到CacheEditor另外一個(gè)應(yīng)用程序或者反方向拖出的能力呈队。

看看Xcode中的關(guān)鍵CacheMaker文件:

  • CachesDataSource.swift:表示地理緩存(geocaches)集合視圖的數(shù)據(jù)源剥槐。
  • CachesViewController.swift:顯示地理緩存的看板。

這些是您將要使用的文件宪摧,用于添加所需的功能粒竖。


Drag and Drop Overview

從源應(yīng)用程序拖動(dòng)項(xiàng)目時(shí),拖動(dòng)活動(dòng)(drag activity)將開始绍刮,系統(tǒng)將創(chuàng)建拖動(dòng)會(huì)話(drag session)温圆。 源應(yīng)用程序(source app)設(shè)置拖動(dòng)項(xiàng)(drag item)以在拖動(dòng)活動(dòng)開始時(shí)表示基礎(chǔ)數(shù)據(jù)。 在目標(biāo)應(yīng)用(destination app)中放置該項(xiàng)結(jié)束拖動(dòng)活動(dòng)孩革。

拖動(dòng)項(xiàng)目封裝在項(xiàng)目提供程序(item provider)中岁歉,該項(xiàng)目提供程序描述源應(yīng)用程序可以提供的數(shù)據(jù)類型。刪除項(xiàng)目后膝蜈,目標(biāo)應(yīng)用程序會(huì)以可以使用的格式請(qǐng)求項(xiàng)目锅移。

Apple自動(dòng)支持拖放文本視圖和文本字段(text views and text fields)。 它還為表視圖和集合視圖(table views and collection views)提供專用API饱搏。 您也可以添加拖放到自定義視圖嘶炭。

在本教程中,您將在集合視圖(collection views)和自定義視圖中探索拖放呛牲。


Adding Drag Support

轉(zhuǎn)到CachesDataSource.swift并將以下擴(kuò)展名添加到文件末尾:

extension CachesDataSource {
  func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
    let geocache = geocaches[indexPath.item]
    let itemProvider = NSItemProvider(object: geocache.name as NSString)
    let dragItem = UIDragItem(itemProvider: itemProvider)
    return [dragItem]
  }
}

在這里议泵,您可以從geocache名稱的NSString表示創(chuàng)建項(xiàng)目提供程序(item provider)。 然后返回一個(gè)包含此項(xiàng)目提供程序的拖動(dòng)項(xiàng)目(drag item)的數(shù)組鬓催。

接下來肺素,打開CachesViewController.swift并將以下內(nèi)容添加到文件末尾:

extension CachesViewController: UICollectionViewDragDelegate {
  func collectionView(_ collectionView: UICollectionView,
                      itemsForBeginning session: UIDragSession,
                      at indexPath: IndexPath) -> [UIDragItem] {
    let dataSource = dataSourceForCollectionView(collectionView)
    return dataSource.dragItems(for: indexPath)
  }
}   

您采用UICollectionViewDragDelegate并實(shí)現(xiàn)在拖動(dòng)活動(dòng)開始時(shí)調(diào)用的required方法。 您的實(shí)現(xiàn)獲取collection view的數(shù)據(jù)源宇驾,然后返回所選項(xiàng)的相應(yīng)拖動(dòng)項(xiàng)倍靡。

collection view代理賦值后,將以下內(nèi)容添加到viewDidLoad()

collectionView.dragDelegate = self  

這使視圖控制器成為drag delegate课舍。

構(gòu)建并運(yùn)行應(yīng)用程序塌西。 點(diǎn)擊并按住代表地理緩存的集合視圖單元格他挎。 點(diǎn)擊的單元應(yīng)該上升,允許您拖動(dòng)它:

請(qǐng)注意捡需,雖然您可以拖動(dòng)項(xiàng)目办桨,但不能將其放在任何位置。 嘗試這樣做只會(huì)將其放回原點(diǎn)站辉。

CacheMaker旁邊的Split View中打開Reminders崔挖。 您應(yīng)該能夠拖動(dòng)geocache并將其放入Reminders

Reminders可以接受geocache名稱的導(dǎo)出NSString表示,并使用它來創(chuàng)建新reminder庵寞。

現(xiàn)在嘗試將Reminders中的任何文本拖動(dòng)到CacheMaker中狸相。 什么都沒發(fā)生。 那是因?yàn)槟鷽]有向CacheMaker添加drop支持捐川。 你接下來要解決這個(gè)問題脓鹃。


Adding Drop Support

轉(zhuǎn)到CachesDataSource.swift并將以下內(nèi)容添加到CachesDataSource擴(kuò)展:

func addGeocache(_ newGeocache: Geocache, at index: Int) {
  geocaches.insert(newGeocache, at: index)
}   

這會(huì)向數(shù)據(jù)源添加新的geocache

切換到CachesViewController.swift并將以下協(xié)議擴(kuò)展添加到結(jié)尾:

extension CachesViewController: UICollectionViewDropDelegate {
  func collectionView(
      _ collectionView: UICollectionView, 
      performDropWith coordinator: UICollectionViewDropCoordinator) {
    // 1
    let dataSource = dataSourceForCollectionView(collectionView)
    // 2
    let destinationIndexPath =
      IndexPath(item: collectionView.numberOfItems(inSection: 0), section: 0)
    // 3
    let item = coordinator.items[0]
  // 4
   switch coordinator.proposal.operation
      {
        case .copy:
          print("Copying...")
          let itemProvider = item.dragItem.itemProvider
         // 5
          itemProvider.loadObject(ofClass: NSString.self) { string, error in
            if let string = string as? String {
              // 6
              let geocache = Geocache(
                name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
              // 7
              dataSource.addGeocache(geocache, at: destinationIndexPath.item)
              // 8
              DispatchQueue.main.async {
                collectionView.insertItems(at: [destinationIndexPath])
              }
            }
          }
        default:
          return
      }
  } 
}

在這里古沥,您采用UICollectionViewDropDelegate協(xié)議瘸右。 然后,您可以實(shí)現(xiàn)在用戶結(jié)束拖動(dòng)活動(dòng)時(shí)調(diào)用的required方法岩齿。 你的實(shí)現(xiàn):

  • 1) 獲取集合視圖(collection view)的數(shù)據(jù)源太颤。
  • 2) 將集合視圖的結(jié)尾設(shè)置為項(xiàng)目放置目標(biāo)。
  • 3) 選擇第一個(gè)拖動(dòng)項(xiàng)盹沈。
  • 4) 檢查你打算如何處理drop龄章。
  • 5) 異步獲取拖動(dòng)項(xiàng)目的數(shù)據(jù)。
  • 6) 使用基于傳入字符串?dāng)?shù)據(jù)的名稱創(chuàng)建新的geocache乞封。
  • 7) 將新geocache添加到數(shù)據(jù)源做裙。
  • 8) 在集合視圖(collection view)中插入新項(xiàng)。 您在主線程上調(diào)用此方法肃晚,因?yàn)閿?shù)據(jù)提取完成塊在內(nèi)部隊(duì)列上運(yùn)行锚贱。

1. Responding to Drops

將以下內(nèi)容添加到UICollectionViewDropDelegate擴(kuò)展的末尾:

func collectionView(
    _ collectionView: UICollectionView,
    dropSessionDidUpdate session: UIDropSession,
    withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UICollectionViewDropProposal {
  if session.localDragSession != nil {
    return UICollectionViewDropProposal(operation: .forbidden)
  } else {
    return UICollectionViewDropProposal(
      operation: .copy,
      intent: .insertAtDestinationIndexPath)
  }
}

您指定對(duì)正在拖動(dòng)的項(xiàng)目的響應(yīng)。 這包括向用戶提供視覺反饋关串。

這里的代碼禁止在應(yīng)用程序中進(jìn)行拖放。 它建議從另一個(gè)應(yīng)用程序中dropped項(xiàng)目的復(fù)制操作晋修。

分配drag delegate后吧碾,將以下內(nèi)容添加到viewDidLoad()

collectionView.dropDelegate = self  

這會(huì)將view controller設(shè)置為drop delegate

構(gòu)建并運(yùn)行應(yīng)用程序飞蚓。 使用Split View中的Reminders滤港,驗(yàn)證您是否可以將reminder拖動(dòng)到正在進(jìn)行的集合視圖中:

如果您嘗試拖放到列表中間廊蜒,您將看到它只會(huì)添加到列表的末尾趴拧。 你以后會(huì)改進(jìn)這個(gè)溅漾。

嘗試在應(yīng)用內(nèi)拖放geocache。 驗(yàn)證您是否獲得了不允許的視覺提示:

這并不理想著榴,所以你接下來就會(huì)繼續(xù)工作添履。


Drag and Drop in the Same App

仍然在CachesViewController.swift中,轉(zhuǎn)到collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)并使用以下代碼替換forbidden return語句:

guard session.items.count == 1 else {
  return UICollectionViewDropProposal(operation: .cancel)
}

if collectionView.hasActiveDrag {
  return UICollectionViewDropProposal(operation: .move,
                                      intent: .insertAtDestinationIndexPath)
} else {
  return UICollectionViewDropProposal(operation: .copy,
                                      intent: .insertAtDestinationIndexPath)
}

如果選擇了多個(gè)項(xiàng)目脑又,代碼將取消drop暮胧。 對(duì)于單個(gè)drop item,如果您在同一個(gè)collection view中问麸,則建議移動(dòng)往衷。 否則,你提出一份copy严卖。

CachesDataSource.swift中席舍,將以下方法添加到擴(kuò)展:

func moveGeocache(at sourceIndex: Int, to destinationIndex: Int) {
  guard sourceIndex != destinationIndex else { return }

  let geocache = geocaches[sourceIndex]
  geocaches.remove(at: sourceIndex)
  geocaches.insert(geocache, at: destinationIndex)
}

這會(huì)在數(shù)據(jù)源中重新定位geocache

返回CachesViewController.swift并在collectionView(_:performDropWith :)中使用以下內(nèi)容替換destinationIndexPath賦值:

let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
  destinationIndexPath = indexPath
} else {
  destinationIndexPath = IndexPath(
    item: collectionView.numberOfItems(inSection: 0), 
    section: 0)
}   

在這里哮笆,檢查index path来颤,指定插入項(xiàng)目的位置。 如果未找到稠肘,則項(xiàng)目將在collection view的末尾插入福铅。

.copy case之前添加以下內(nèi)容:

case .move:
  print("Moving...")
  // 1
  if let sourceIndexPath = item.sourceIndexPath {
    // 2
    collectionView.performBatchUpdates({
      dataSource.moveGeocache(
        at: sourceIndexPath.item,
        to: destinationIndexPath.item)
      collectionView.deleteItems(at: [sourceIndexPath])
      collectionView.insertItems(at: [destinationIndexPath])
    })
    // 3
    coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
  }

這段代碼:

  • 1) 獲取您應(yīng)該有權(quán)訪問的源index path,以便在同一個(gè)集合視圖中進(jìn)行拖放项阴。
  • 2) 執(zhí)行批量更新以在數(shù)據(jù)源和集合視圖中移動(dòng)geocache滑黔。
  • 3) 在集合視圖中動(dòng)畫插入拖動(dòng)的geocache

1. Follow My Moves

構(gòu)建并運(yùn)行應(yīng)用程序环揽。 驗(yàn)證在集合視圖中拖放geocache是否會(huì)創(chuàng)建副本并記錄副本消息:

測試您還可以在同一個(gè)集合視圖中移動(dòng)geocache并查看記錄的移動(dòng)消息:

在集合視圖中拖放時(shí)拷沸,您可能已經(jīng)發(fā)現(xiàn)了一些效率低下的問題。 您正在使用同一個(gè)應(yīng)用程序薯演,但您正在創(chuàng)建該對(duì)象的低保真副本撞芍。 更不用說,你正在創(chuàng)建一個(gè)副本跨扮!

當(dāng)然序无,你可以做得更好。


Optimizing the Drop Experience

您可以進(jìn)行一些優(yōu)化以改進(jìn)拖放實(shí)現(xiàn)和體驗(yàn)衡创。

1. Using In-Memory Data

您應(yīng)該利用您在同一個(gè)應(yīng)用程序中訪問完整geocache結(jié)構(gòu)的權(quán)限帝嗡。

轉(zhuǎn)到CachesDataSource.swift。 在return語句之前直接將以下內(nèi)容添加到dragItems(for :)

dragItem.localObject = geocache

您將geocache分配給drag item屬性璃氢。 這樣可以在以后更快地檢索項(xiàng)目

轉(zhuǎn)到CachesViewController.swift哟玷。 在collectionView(_:performDropWith :)中,使用以下內(nèi)容替換.copy case中的代碼:

if let geocache = item.dragItem.localObject as? Geocache {
  print("Copying from same app...")
  dataSource.addGeocache(geocache, at: destinationIndexPath.item)
  DispatchQueue.main.async {
    collectionView.insertItems(at: [destinationIndexPath])
  }
} else {
  print("Copying from different app...")
  let itemProvider = item.dragItem.itemProvider
  itemProvider.loadObject(ofClass: NSString.self) { string, error in
    if let string = string as? String {
      let geocache = Geocache(
        name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
      dataSource.addGeocache(geocache, at: destinationIndexPath.item)
      DispatchQueue.main.async {
        collectionView.insertItems(at: [destinationIndexPath])
      }
    }
  }
}   

這里,處理從不同應(yīng)用程序中dropped的項(xiàng)目的代碼沒有改變巢寡。 對(duì)于從同一個(gè)應(yīng)用程序復(fù)制的項(xiàng)目喉脖,您可以從localObject獲取已保存的geocache并使用它來創(chuàng)建新的geocache

構(gòu)建并運(yùn)行應(yīng)用程序抑月。 驗(yàn)證跨collections view的拖放現(xiàn)在復(fù)制geocache結(jié)構(gòu):

2. Moving Items Across Collection Views

您現(xiàn)在可以更好地表示geocache树叽。 這很好,但你真的應(yīng)該在collection views中移動(dòng)geocache而不是復(fù)制它谦絮。

仍然在CachesViewController.swift中题诵,使用以下內(nèi)容替換collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)實(shí)現(xiàn):

guard session.localDragSession != nil else {
  return UICollectionViewDropProposal(
      operation: .copy,
      intent: .insertAtDestinationIndexPath)
}
guard session.items.count == 1 else {
  return UICollectionViewDropProposal(operation: .cancel)
}
return UICollectionViewDropProposal(
    operation: .move,
    intent: .insertAtDestinationIndexPath)

您現(xiàn)在可以在與移動(dòng)操作同一個(gè)應(yīng)用程序中處理drops

轉(zhuǎn)到File ? New ? File…并選擇iOS ? Source ? Swift File模板层皱。 點(diǎn)擊Next性锭。 將文件命名為CacheDragCoordinator.swift,然后單擊Create叫胖。

在文件末尾添加以下內(nèi)容:

class CacheDragCoordinator {
  let sourceIndexPath: IndexPath
  var dragCompleted = false
  var isReordering = false
  
  init(sourceIndexPath: IndexPath) {
    self.sourceIndexPath = sourceIndexPath
  }
}   

您已經(jīng)創(chuàng)建了一個(gè)類來協(xié)調(diào)同一個(gè)應(yīng)用程序中的拖放篷店。 在這里設(shè)置要跟蹤的屬性:

  • 拖動(dòng)開始的地方。
  • 什么時(shí)候完成臭家。
  • 集合視圖項(xiàng)應(yīng)該在drop后重新排序疲陕。

切換到CachesDataSource.swift并將以下方法添加到擴(kuò)展:

func deleteGeocache(at index: Int) {
  geocaches.remove(at: index)
}   

此方法刪除指定索引處的geocache。 重新排序集合視圖項(xiàng)時(shí)钉赁,您將使用此幫助方法蹄殃。

轉(zhuǎn)到CachesViewController.swift。 在return語句之前直接將以下內(nèi)容添加到collectionView(_:itemsForBeginning:at)

let dragCoordinator = CacheDragCoordinator(sourceIndexPath: indexPath)
session.localContext = dragCoordinator  

在這里你踩,使用起始index path初始化drag coordinator诅岩。 然后,將此對(duì)象添加到存儲(chǔ)自定義數(shù)據(jù)的drag會(huì)話屬性带膜。 此數(shù)據(jù)僅對(duì)拖動(dòng)活動(dòng)開始的應(yīng)用程序可見吩谦。

3. Are You My App?

找到collectionView(_:performDropWith :)。 使用以下內(nèi)容替換.copy case中的代碼:

print("Copying from different app...")
let itemProvider = item.dragItem.itemProvider
itemProvider.loadObject(ofClass: NSString.self) { string, error in
  if let string = string as? String {
    let geocache = Geocache(
      name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
    dataSource.addGeocache(geocache, at: destinationIndexPath.item)
    DispatchQueue.main.async {
      collectionView.insertItems(at: [destinationIndexPath])
    }
  }
}

您已將復(fù)制路徑簡化為僅處理來自其他應(yīng)用的drops膝藕。

用以下內(nèi)容替換.move case中的代碼:

// 1
guard let dragCoordinator =
  coordinator.session.localDragSession?.localContext as? CacheDragCoordinator
  else { return }
// 2
if let sourceIndexPath = item.sourceIndexPath {
  print("Moving within the same collection view...")
  // 3
  dragCoordinator.isReordering = true
  // 4
  collectionView.performBatchUpdates({
    dataSource.moveGeocache(at: sourceIndexPath.item, to: destinationIndexPath.item)
    collectionView.deleteItems(at: [sourceIndexPath])
    collectionView.insertItems(at: [destinationIndexPath])
  })
} else {
  print("Moving between collection views...")
  // 5
  dragCoordinator.isReordering = false
  // 6
  if let geocache = item.dragItem.localObject as? Geocache {
    collectionView.performBatchUpdates({
      dataSource.addGeocache(geocache, at: destinationIndexPath.item)
      collectionView.insertItems(at: [destinationIndexPath])
    })
  }
}
// 7
dragCoordinator.dragCompleted = true
// 8
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)

這是對(duì)正在發(fā)生的事情的逐步細(xì)分:

  • 1) 獲得drag coordinator式廷。
  • 2) 檢查是否設(shè)置了拖動(dòng)項(xiàng)的源索引路徑source index path。 這意味著拖放位于同一個(gè)集合視圖中芭挽。
  • 3) 通知drag coordinator將重新排序集合視圖滑废。
  • 4) 執(zhí)行批量更新以在數(shù)據(jù)源和集合視圖中移動(dòng)geocache
  • 5) 請(qǐng)注意袜爪,集合視圖不會(huì)被重新排序蠕趁。
  • 6) 檢索本地存儲(chǔ)的geocache。 將其添加到數(shù)據(jù)源并將其插入到集合視圖中辛馆。
  • 7) 讓drag coordinator知道拖動(dòng)完成了俺陋。
  • 8) 動(dòng)畫在集合視圖中拖動(dòng)的geocache的插入。

將以下方法添加到您的UICollectionViewDragDelegate擴(kuò)展:

func collectionView(_ collectionView: UICollectionView,
                    dragSessionDidEnd session: UIDragSession) {
  // 1
  guard 
    let dragCoordinator = session.localContext as? CacheDragCoordinator,
    dragCoordinator.dragCompleted == true,
    dragCoordinator.isReordering == false 
    else { 
      return 
    }
  // 2
  let dataSource = dataSourceForCollectionView(collectionView)
  let sourceIndexPath = dragCoordinator.sourceIndexPath
  // 3
  collectionView.performBatchUpdates({
    dataSource.deleteGeocache(at: sourceIndexPath.item)
    collectionView.deleteItems(at: [sourceIndexPath])
  })
}   

當(dāng)拖動(dòng)中止或drop項(xiàng)目時(shí),將調(diào)用此方法腊状。 這是代碼的作用:

  • 1) 檢查drag coordinator诱咏。 如果drop完成且集合視圖未重新排序,則繼續(xù)寿酌。
  • 2) 獲取數(shù)據(jù)源和源索引路徑以準(zhǔn)備更新。
  • 3) 執(zhí)行批量更新以從數(shù)據(jù)源和集合視圖中刪除geocache硕蛹。 回想一下醇疼,您之前已將相同的geocache添加到drop destination。 這需要將其從drag source上移除法焰。

構(gòu)建并運(yùn)行應(yīng)用程序秧荆。 驗(yàn)證在集合視圖中移動(dòng)實(shí)際上是否移動(dòng)項(xiàng)目并在控制臺(tái)打印Moving between collection views...

4. Adding a Placeholder

從外部應(yīng)用程序獲取項(xiàng)目并將其加載到目標(biāo)應(yīng)用程序中可能需要一些時(shí)間。 向用戶提供視覺反饋(例如顯示占位符)是一種很好的做法埃仪。

用以下代碼替換collectionView(_:performDropWith :)中的.copy case

print("Copying from different app...")
// 1
let placeholder = UICollectionViewDropPlaceholder(
  insertionIndexPath: destinationIndexPath, reuseIdentifier: "CacheCell")
// 2
placeholder.cellUpdateHandler = { cell in
  if let cell = cell as? CacheCell {
    cell.cacheNameLabel.text = "Loading..."
    cell.cacheSummaryLabel.text = ""
    cell.cacheImageView.image = nil
  }
}
// 3
let context = coordinator.drop(item.dragItem, to: placeholder)
let itemProvider = item.dragItem.itemProvider
itemProvider.loadObject(ofClass: NSString.self) { string, error in
  if let string = string as? String {
    let geocache = Geocache(
      name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
    // 4
    DispatchQueue.main.async {
      context.commitInsertion(dataSourceUpdates: {_ in
        dataSource.addGeocache(geocache, at: destinationIndexPath.item)
      })
    }
  }
}

這是正在發(fā)生的事情:

  • 1) 為新內(nèi)容創(chuàng)建占位符單元格乙濒。
  • 2) 定義配置占位符單元格的block
  • 3) 將占位符插入集合視圖中卵蛉。
  • 4) 提交插入以將占位符與最終單元格交換颁股。

構(gòu)建并運(yùn)行應(yīng)用程序。 從Reminders中拖放項(xiàng)目傻丝。 將項(xiàng)目放入集合視圖時(shí)甘有,請(qǐng)注意占位符文本的簡要外觀:


Multiple Data Representations

您可以配置可以傳遞到目標(biāo)應(yīng)用程序或從源應(yīng)用程序使用的數(shù)據(jù)類型。

使用init(object :)創(chuàng)建項(xiàng)目提供程序item provider時(shí)葡缰,傳入的對(duì)象必須符合NSItemProviderWriting亏掀。 采用該協(xié)議包括為您可以導(dǎo)出的數(shù)據(jù)指定統(tǒng)一類型標(biāo)識(shí)符(uniform type identifiers - UTI)并處理每個(gè)數(shù)據(jù)表示的導(dǎo)出。

例如泛释,您可能希望為僅接受字符串的應(yīng)用導(dǎo)出geocache的字符串表示形式滤愕。 或者您可能想要導(dǎo)出照片應(yīng)用的圖像表示。 對(duì)于您控制下使用地理位置的應(yīng)用怜校,您可能希望導(dǎo)出完整的數(shù)據(jù)模型间影。

要正確使用已dropped的項(xiàng)并將其轉(zhuǎn)換為geocaches,您的數(shù)據(jù)模型應(yīng)采用NSItemProviderReading茄茁。 然后宇智,您可以實(shí)現(xiàn)協(xié)議方法以指定可以使用的數(shù)據(jù)表示形式。 您還將實(shí)現(xiàn)它們以指定如何根據(jù)源應(yīng)用程序發(fā)送的內(nèi)容強(qiáng)制傳入數(shù)據(jù)胰丁。

到目前為止随橘,在應(yīng)用程序之間拖放地理緩存時(shí),您已經(jīng)使用過字符串锦庸。 NSString自動(dòng)支持NSItemProviderWritingNSItemProviderReading机蔗,因此您不必編寫任何特殊代碼。

要處理多種數(shù)據(jù)類型,您將更改geocache數(shù)據(jù)模型萝嘁。 您可以在Geocache項(xiàng)目中找到它梆掸,它是您打開的Xcode工作區(qū)的一部分。

Geocache項(xiàng)目中牙言,打開Geocache.swift并在Foundation import后添加以下內(nèi)容:

import MobileCoreServices

您需要此框架來使用預(yù)定義的UTI酸钦,例如代表PNGUTI

在上次導(dǎo)入后立即添加以下內(nèi)容:

public let geocacheTypeId = "com.xxxxx.geocache" 

您創(chuàng)建一個(gè)代表geocache的自定義字符串標(biāo)識(shí)符咱枉。

1. Reading and Writing Geocaches

將以下擴(kuò)展名添加到文件末尾:

extension Geocache: NSItemProviderWriting {
  // 1
  public static var writableTypeIdentifiersForItemProvider: [String] {
    return [geocacheTypeId,
            kUTTypePNG as String,
            kUTTypePlainText as String]
  }
  // 2
  public func loadData(
    withTypeIdentifier typeIdentifier: String,
    forItemProviderCompletionHandler completionHandler:
    @escaping (Data?, Error?) -> Void) 
      -> Progress? {
    if typeIdentifier == kUTTypePNG as String {
      // 3
      if let image = image {
        completionHandler(image, nil)
      } else {
        completionHandler(nil, nil)
      }
    } else if typeIdentifier == kUTTypePlainText as String {
      // 4
      completionHandler(name.data(using: .utf8), nil)
    } else if typeIdentifier == geocacheTypeId {
      // 5
      do {        
        let archiver = NSKeyedArchiver(requiringSecureCoding: false)
        try archiver.encodeEncodable(self, forKey: NSKeyedArchiveRootObjectKey)
        archiver.finishEncoding()
        let data = archiver.encodedData
        
        completionHandler(data, nil)
      } catch {
        completionHandler(nil, nil)
      }
    }
    return nil
  }
}   

在這里卑硫,您符合NSItemProviderWriting并執(zhí)行以下操作:

  • 1) 指定可以傳送到目標(biāo)應(yīng)用程序的數(shù)據(jù)表示。 您希望返回從對(duì)象的最高保真度版本到最低值排序的字符串?dāng)?shù)組蚕断。
  • 2) 實(shí)現(xiàn)在請(qǐng)求時(shí)將數(shù)據(jù)傳遞到目標(biāo)應(yīng)用程序的方法欢伏。 系統(tǒng)在dropped項(xiàng)目時(shí)會(huì)調(diào)用此方法并傳入適當(dāng)?shù)念愋蜆?biāo)識(shí)符。
  • 3) 如果傳入PNG標(biāo)識(shí)符亿乳,則在完成處理程序中返回geocache的圖像硝拧。
  • 4) 如果傳入文本標(biāo)識(shí)符,則在完成處理程序中返回geocache的名稱葛假。
  • 5) 如果傳入自定義geocache類型標(biāo)識(shí)符障陶,則返回與整個(gè)geocache對(duì)應(yīng)的數(shù)據(jù)對(duì)象。

現(xiàn)在聊训,在分配geocacheTypeId后立即添加以下枚舉:

enum EncodingError: Error {
  case invalidData
}

當(dāng)讀取數(shù)據(jù)時(shí)出現(xiàn)問題咸这,您將使用它來返回錯(cuò)誤代碼。

接下來魔眨,將以下內(nèi)容添加到文件末尾:

extension Geocache: NSItemProviderReading {
  // 1
  public static var readableTypeIdentifiersForItemProvider: [String] {
    return [geocacheTypeId,
            kUTTypePlainText as String]
  }
  // 2
  public static func object(withItemProviderData data: Data,
                            typeIdentifier: String) throws -> Self {
    if typeIdentifier == kUTTypePlainText as String {
      // 3
      guard let name = String(data: data, encoding: .utf8) else {
        throw EncodingError.invalidData
      }
      return self.init(
        name: name, 
        summary: "Unknown", 
        latitude: 0.0, 
        longitude: 0.0)
    } else if typeIdentifier == geocacheTypeId {
      // 4
      do {
        let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
        guard let geocache =
          try unarchiver.decodeTopLevelDecodable(
            Geocache.self, forKey: NSKeyedArchiveRootObjectKey) else {
              throw EncodingError.invalidData
        }
        return self.init(geocache)
      } catch {
        throw EncodingError.invalidData
      }
    } else {
      throw EncodingError.invalidData
    }
  }
}   

在這里媳维,您遵循NSItemProviderReading以指定如何處理傳入數(shù)據(jù)。 這是正在做的事情:

  • 1) 指定模型可以使用的傳入數(shù)據(jù)的類型遏暴。 此處列出的UTI代表geocache和文本侄刽。
  • 2) 給定類型標(biāo)識(shí)符,實(shí)現(xiàn)導(dǎo)入數(shù)據(jù)required的協(xié)議方法朋凉。
  • 3) 對(duì)于文本標(biāo)識(shí)符州丹,使用基于傳入文本和占位符信息的名稱創(chuàng)建新的地理緩存。
  • 4) 對(duì)于geocache標(biāo)識(shí)符杂彭,解碼傳入的數(shù)據(jù)并使用它來創(chuàng)建完整的geocache模型墓毒。

錯(cuò)誤或無法識(shí)別的類型標(biāo)識(shí)符會(huì)引發(fā)您之前定義的錯(cuò)誤。

2. Back to My App

active scheme更改為Geocache并構(gòu)建項(xiàng)目亲怠。 然后將active scheme更改回CacheMaker所计。

CacheMaker中,轉(zhuǎn)到CachesDataSource.swift并在dragItems(for:)內(nèi)部將itemProvider賦值更改為:

let itemProvider = NSItemProvider(object: geocache) 

在這里团秽,您可以使用geocache初始化項(xiàng)目提供程序主胧,因?yàn)槟哪P筒捎?code>NSItemProviderWriting來正確導(dǎo)出數(shù)據(jù)叭首。

打開CachesViewController.swift并找到collectionView(_:performDropWith :)。 在.copy case下踪栋,使用以下內(nèi)容替換item providerloadObject調(diào)用:

itemProvider.loadObject(ofClass: Geocache.self) { geocache, _ in
  if let geocache = geocache as? Geocache {
    DispatchQueue.main.async {
        context.commitInsertion(dataSourceUpdates: {_ in
          dataSource.addGeocache(geocache, at: destinationIndexPath.item)
      })
    }
  }
}

您已修改了drop handler以加載Geocache類型的對(duì)象焙格。 完成塊現(xiàn)在返回一個(gè)可以直接使用的geocache

構(gòu)建并運(yùn)行應(yīng)用程序夷都。 如有必要眷唉,將Reminders放在Split View中。 檢查RemindersCacheMaker之間的拖放項(xiàng)是否像以前一樣工作:

Split View中顯示Photos以替換Reminders囤官。 從正在進(jìn)行的通道拖動(dòng)地理緩存并將其放入Photos以驗(yàn)證您是否可以導(dǎo)出地理緩存的圖像表示:

您可以使用臨時(shí)hack測試完整數(shù)據(jù)模型導(dǎo)出路徑冬阳。 轉(zhuǎn)到CachesViewController.swift并在collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)中使用以下內(nèi)容替換返回移動(dòng)操作的行:

return UICollectionViewDropProposal(
  operation: .copy, 
  intent: .insertAtDestinationIndexPath)

您在與復(fù)制操作相同的應(yīng)用程序中配置拖放。 這會(huì)觸發(fā)應(yīng)導(dǎo)出和導(dǎo)入完整數(shù)據(jù)模型的代碼治拿。

構(gòu)建并運(yùn)行應(yīng)用程序摩泪。 測試在應(yīng)用程序中移動(dòng)項(xiàng)目會(huì)生成geocache的正確副本:

collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)中恢復(fù)臨時(shí)hack笆焰,以便應(yīng)用程序內(nèi)拖放執(zhí)行移動(dòng)操作:

return UICollectionViewDropProposal(
  operation: .move, 
  intent: .insertAtDestinationIndexPath)    

構(gòu)建并運(yùn)行應(yīng)用程序以恢復(fù)到pre-hack的狀態(tài)劫谅。


Adding Drag Support to a Custom View

您已經(jīng)了解了如何向集合視圖添加拖放支持。 將此支持添加到table views遵循類似的過程嚷掠。

您還可以向自定義視圖添加拖放功能捏检。 基本步驟包括:

  • 將交互對(duì)象添加到自定義視圖。
  • 在交互委托中實(shí)現(xiàn)協(xié)議方法以提供或使用數(shù)據(jù)不皆。

是時(shí)候介紹用于編輯地理藏寶的伴侶應(yīng)用程序CacheEditor贯城。 將active scheme更改為
CacheEditor。 構(gòu)建并運(yùn)行應(yīng)用程序霹娄,然后將設(shè)備旋轉(zhuǎn)到橫向模式:

Split View中查看CacheMaker能犯,將其放在CacheEditor的左側(cè)。 調(diào)整Split View的大小犬耻,使兩個(gè)應(yīng)用程序占用大約一半的寬度:

嘗試將geocacheCacheEditor拖到CacheMaker中踩晶。 我的朋友,你是一個(gè)令人沮喪的經(jīng)歷枕磁。

您將使用CacheEditor中的一個(gè)關(guān)鍵文件CacheDetailViewController.swift渡蜻,它顯示geocache詳細(xì)信息。 打開該文件并將以下代碼添加到最后:

// MARK: - UIDragInteractionDelegate
extension CacheDetailViewController: UIDragInteractionDelegate {
  func dragInteraction(
    _ interaction: UIDragInteraction, 
    itemsForBeginning session: UIDragSession) 
      -> [UIDragItem] {
    let itemProvider = NSItemProvider(object: geocache)
    let dragItem = UIDragItem(itemProvider: itemProvider)
    return [ dragItem ]
  }
}

在這里计济,您采用UIDragInteractionDelegate并實(shí)現(xiàn)拖動(dòng)活動(dòng)開始時(shí)調(diào)用的方法茸苇。 代碼應(yīng)該與您在CacheMaker中看到的類似。 您將帶有geocache的拖動(dòng)項(xiàng)目作為item provider返回沦寂。

在調(diào)用super之后立即將以下內(nèi)容添加到viewDidLoad()

view.addInteraction(UIDragInteraction(delegate: self))  

在這里学密,您將創(chuàng)建與作為代理的視圖控制器的拖動(dòng)交互。 然后传藏,您將交互添加到視圖中则果。

構(gòu)建并運(yùn)行CacheEditor幔翰。 驗(yàn)證您現(xiàn)在可以從CacheEditor拖動(dòng)地理緩存并將其放入CacheMaker

嘗試將地理緩存從CacheMaker拖到CacheEditor中。 拖動(dòng)開始時(shí)西壮,它不會(huì)drop遗增。 這是你的下一個(gè)任務(wù)。


Adding Drop Support to a Custom View

仍然在CacheDetailViewController.swift中款青,將以下內(nèi)容添加到文件的末尾:

// MARK: - UIDropInteractionDelegate
extension CacheDetailViewController : UIDropInteractionDelegate {
  func dropInteraction(
    _ interaction: UIDropInteraction, 
    canHandle session: UIDropSession) 
      -> Bool {
    return session.canLoadObjects(ofClass: Geocache.self)
  }

  func dropInteraction(
    _ interaction: UIDropInteraction, 
    sessionDidUpdate session: UIDropSession) 
      -> UIDropProposal {
    return UIDropProposal(operation: .copy)
  }

  func dropInteraction(
      _ interaction: UIDropInteraction, 
      performDrop session: UIDropSession) {
    session.loadObjects(ofClass: Geocache.self) { items in
      if let geocaches = items as? [Geocache],
        let geocache = geocaches.first {
        self.geocache = geocache
        self.configureView()
      }
    }
  }
}

在這里做修,您采用UIDropInteractionDelegate并實(shí)現(xiàn)可選optional方法來跟蹤和正確處理drops

第一種方法將drop限制為傳入Geocache對(duì)象的應(yīng)用程序抡草。

第二種方法返回復(fù)制操作作為處理drop的建議方法饰及。 當(dāng)用戶在drop交互視圖上拖動(dòng)項(xiàng)目時(shí),將調(diào)用此方法康震。 雖然此協(xié)議方法是可選的燎含,您也需要實(shí)現(xiàn)它以接受drops

drop手勢完成時(shí)調(diào)用最后一個(gè)協(xié)議方法腿短。 您從會(huì)話中獲取item provider并啟動(dòng)數(shù)據(jù)提取屏箍。 然后,加載第一個(gè)geocache并更新視圖橘忱。

接下來赴魁,在拖動(dòng)交互設(shè)置之后將此代碼添加到viewDidLoad()

view.addInteraction(UIDropInteraction(delegate: self))  

通過此操作,您可以創(chuàng)建drop交互并將視圖控制器設(shè)置為委托钝诚。 然后颖御,您將交互添加到視圖中。

構(gòu)建并運(yùn)行應(yīng)用程序凝颇。 驗(yàn)證您是否可以將geocache dropCacheEditor中:

只需很少的代碼行潘拱,您就可以向自定義視圖添加拖放支持。

恭喜拧略! 您已使用拖放操作來使geocache管理示例應(yīng)用程序正常運(yùn)行芦岂。

您現(xiàn)在應(yīng)該能夠添加拖放功能,以增強(qiáng)許多應(yīng)用內(nèi)和跨應(yīng)用體驗(yàn)了辑鲤。

后記

本篇主要講述了基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例盔腔,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市月褥,隨后出現(xiàn)的幾起案子弛随,更是在濱河造成了極大的恐慌,老刑警劉巖宁赤,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舀透,死亡現(xiàn)場離奇詭異,居然都是意外死亡决左,警方通過查閱死者的電腦和手機(jī)愕够,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門走贪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惑芭,你說我怎么就攤上這事坠狡。” “怎么了遂跟?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵逃沿,是天一觀的道長。 經(jīng)常有香客問我幻锁,道長凯亮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任哄尔,我火速辦了婚禮假消,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘岭接。我一直安慰自己富拗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布亿傅。 她就那樣靜靜地躺著媒峡,像睡著了一般瘟栖。 火紅的嫁衣襯著肌膚如雪葵擎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天半哟,我揣著相機(jī)與錄音酬滤,去河邊找鬼。 笑死寓涨,一個(gè)胖子當(dāng)著我的面吹牛盯串,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播戒良,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼体捏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了糯崎?” 一聲冷哼從身側(cè)響起几缭,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沃呢,沒想到半個(gè)月后年栓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薄霜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年某抓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纸兔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡否副,死狀恐怖汉矿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情备禀,我是刑警寧澤负甸,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站痹届,受9級(jí)特大地震影響呻待,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜队腐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一蚕捉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柴淘,春花似錦迫淹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至第股,卻和暖如春应民,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夕吻。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工诲锹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涉馅。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓归园,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稚矿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子庸诱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容