UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)

版本记录

版本号 时间
V1.0 2019.08.13 星期二

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)

开始

首先看下主要内容

主要内容:在这个drag and drop教程中,您将构建支持UICollectionViews和两个单独的iOS应用程序之间的drag and drop

下面看一下写作环境

Swift 5, iOS 12, Xcode 10

Apple在iOS 11中引入了拖放(drag and drop)功能,允许用户将项目从一个屏幕位置拖动到另一个屏幕位置。 在iPhone上,拖放仅在应用程序内可用,而在iPad上,它也可以跨应用程序使用。 这对于快速将Photos中的图像添加到电子邮件中非常方便。

在本教程中,您将通过构建CacheManager来探索拖放,这两个应用程序用于管理地理缓存:

CacheMaker在正在进行和已完成项目的看板中组织地理藏宝。 CacheEditor允许用户编辑从CacheMaker带来的geocache的详细信息。 您将通过向两个应用添加拖放支持来实现这些管理功能。

在Xcode中打开CacheManager.xcworkspace并选择CacheMaker作为active scheme

构建并运行CacheMaker。 您应该看到两个collection views,其中第一个包含用于正在进行的工作的地理缓存:

尝试将geocache从正在进行的栏拖动到完成的栏:

在本教程的第一部分中,您的目标是实现这一目标。 稍后,您将解锁将geocaches拖放到CacheEditor另外一个应用程序或者反方向拖出的能力。

看看Xcode中的关键CacheMaker文件:

  • CachesDataSource.swift:表示地理缓存(geocaches)集合视图的数据源。
  • CachesViewController.swift:显示地理缓存的看板。

这些是您将要使用的文件,用于添加所需的功能。


Drag and Drop Overview

从源应用程序拖动项目时,拖动活动(drag activity)将开始,系统将创建拖动会话(drag session)。 源应用程序(source app)设置拖动项(drag item)以在拖动活动开始时表示基础数据。 在目标应用(destination app)中放置该项结束拖动活动。

拖动项目封装在项目提供程序(item provider)中,该项目提供程序描述源应用程序可以提供的数据类型。删除项目后,目标应用程序会以可以使用的格式请求项目。

Apple自动支持拖放文本视图和文本字段(text views and text fields)。 它还为表视图和集合视图(table views and collection views)提供专用API。 您也可以添加拖放到自定义视图。

在本教程中,您将在集合视图(collection views)和自定义视图中探索拖放。


Adding Drag Support

转到CachesDataSource.swift并将以下扩展名添加到文件末尾:

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表示创建项目提供程序(item provider)。 然后返回一个包含此项目提供程序的拖动项目(drag item)的数组。

接下来,打开CachesViewController.swift并将以下内容添加到文件末尾:

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

您采用UICollectionViewDragDelegate并实现在拖动活动开始时调用的required方法。 您的实现获取collection view的数据源,然后返回所选项的相应拖动项。

collection view代理赋值后,将以下内容添加到viewDidLoad()

collectionView.dragDelegate = self  

这使视图控制器成为drag delegate

构建并运行应用程序。 点击并按住代表地理缓存的集合视图单元格。 点击的单元应该上升,允许您拖动它:

请注意,虽然您可以拖动项目,但不能将其放在任何位置。 尝试这样做只会将其放回原点。

CacheMaker旁边的Split View中打开Reminders。 您应该能够拖动geocache并将其放入Reminders

Reminders可以接受geocache名称的导出NSString表示,并使用它来创建新reminder

现在尝试将Reminders中的任何文本拖动到CacheMaker中。 什么都没发生。 那是因为您没有向CacheMaker添加drop支持。 你接下来要解决这个问题。


Adding Drop Support

转到CachesDataSource.swift并将以下内容添加到CachesDataSource扩展:

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

这会向数据源添加新的geocache

切换到CachesViewController.swift并将以下协议扩展添加到结尾:

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协议。 然后,您可以实现在用户结束拖动活动时调用的required方法。 你的实现:

  • 1) 获取集合视图(collection view)的数据源。
  • 2) 将集合视图的结尾设置为项目放置目标。
  • 3) 选择第一个拖动项。
  • 4) 检查你打算如何处理drop
  • 5) 异步获取拖动项目的数据。
  • 6) 使用基于传入字符串数据的名称创建新的geocache
  • 7) 将新geocache添加到数据源。
  • 8) 在集合视图(collection view)中插入新项。 您在主线程上调用此方法,因为数据提取完成块在内部队列上运行。

1. Responding to Drops

将以下内容添加到UICollectionViewDropDelegate扩展的末尾:

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)
  }
}

您指定对正在拖动的项目的响应。 这包括向用户提供视觉反馈。

这里的代码禁止在应用程序中进行拖放。 它建议从另一个应用程序中dropped项目的复制操作。

分配drag delegate后,将以下内容添加到viewDidLoad()

collectionView.dropDelegate = self  

这会将view controller设置为drop delegate

构建并运行应用程序。 使用Split View中的Reminders,验证您是否可以将reminder拖动到正在进行的集合视图中:

如果您尝试拖放到列表中间,您将看到它只会添加到列表的末尾。 你以后会改进这个。

尝试在应用内拖放geocache。 验证您是否获得了不允许的视觉提示:

这并不理想,所以你接下来就会继续工作。


Drag and Drop in the Same App

仍然在CachesViewController.swift中,转到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)
}

如果选择了多个项目,代码将取消drop。 对于单个drop item,如果您在同一个collection view中,则建议移动。 否则,你提出一份copy

CachesDataSource.swift中,将以下方法添加到扩展:

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)
}

这会在数据源中重新定位geocache

返回CachesViewController.swift并在collectionView(_:performDropWith :)中使用以下内容替换destinationIndexPath赋值:

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

在这里,检查index path,指定插入项目的位置。 如果未找到,则项目将在collection view的末尾插入。

.copy case之前添加以下内容:

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) 获取您应该有权访问的源index path,以便在同一个集合视图中进行拖放。
  • 2) 执行批量更新以在数据源和集合视图中移动geocache
  • 3) 在集合视图中动画插入拖动的geocache

1. Follow My Moves

构建并运行应用程序。 验证在集合视图中拖放geocache是否会创建副本并记录副本消息:

测试您还可以在同一个集合视图中移动geocache并查看记录的移动消息:

在集合视图中拖放时,您可能已经发现了一些效率低下的问题。 您正在使用同一个应用程序,但您正在创建该对象的低保真副本。 更不用说,你正在创建一个副本!

当然,你可以做得更好。


Optimizing the Drop Experience

您可以进行一些优化以改进拖放实现和体验。

1. Using In-Memory Data

您应该利用您在同一个应用程序中访问完整geocache结构的权限。

转到CachesDataSource.swift。 在return语句之前直接将以下内容添加到dragItems(for :)

dragItem.localObject = geocache

您将geocache分配给drag item属性。 这样可以在以后更快地检索项目

转到CachesViewController.swift。 在collectionView(_:performDropWith :)中,使用以下内容替换.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])
      }
    }
  }
}   

这里,处理从不同应用程序中dropped的项目的代码没有改变。 对于从同一个应用程序复制的项目,您可以从localObject获取已保存的geocache并使用它来创建新的geocache

构建并运行应用程序。 验证跨collections view的拖放现在复制geocache结构:

2. Moving Items Across Collection Views

您现在可以更好地表示geocache。 这很好,但你真的应该在collection views中移动geocache而不是复制它。

仍然在CachesViewController.swift中,使用以下内容替换collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)实现:

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)

您现在可以在与移动操作同一个应用程序中处理drops

转到File ▸ New ▸ File…并选择iOS ▸ Source ▸ Swift File模板。 点击Next。 将文件命名为CacheDragCoordinator.swift,然后单击Create

在文件末尾添加以下内容:

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

您已经创建了一个类来协调同一个应用程序中的拖放。 在这里设置要跟踪的属性:

  • 拖动开始的地方。
  • 什么时候完成。
  • 集合视图项应该在drop后重新排序。

切换到CachesDataSource.swift并将以下方法添加到扩展:

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

此方法删除指定索引处的geocache。 重新排序集合视图项时,您将使用此帮助方法。

转到CachesViewController.swift。 在return语句之前直接将以下内容添加到collectionView(_:itemsForBeginning:at)

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

在这里,使用起始index path初始化drag coordinator。 然后,将此对象添加到存储自定义数据的drag会话属性。 此数据仅对拖动活动开始的应用程序可见。

3. Are You My App?

找到collectionView(_:performDropWith :)。 使用以下内容替换.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])
    }
  }
}

您已将复制路径简化为仅处理来自其他应用的drops

用以下内容替换.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)

这是对正在发生的事情的逐步细分:

  • 1) 获得drag coordinator
  • 2) 检查是否设置了拖动项的源索引路径source index path。 这意味着拖放位于同一个集合视图中。
  • 3) 通知drag coordinator将重新排序集合视图。
  • 4) 执行批量更新以在数据源和集合视图中移动geocache
  • 5) 请注意,集合视图不会被重新排序。
  • 6) 检索本地存储的geocache。 将其添加到数据源并将其插入到集合视图中。
  • 7) 让drag coordinator知道拖动完成了。
  • 8) 动画在集合视图中拖动的geocache的插入。

将以下方法添加到您的UICollectionViewDragDelegate扩展:

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])
  })
}   

当拖动中止或drop项目时,将调用此方法。 这是代码的作用:

  • 1) 检查drag coordinator。 如果drop完成且集合视图未重新排序,则继续。
  • 2) 获取数据源和源索引路径以准备更新。
  • 3) 执行批量更新以从数据源和集合视图中删除geocache。 回想一下,您之前已将相同的geocache添加到drop destination。 这需要将其从drag source上移除。

构建并运行应用程序。 验证在集合视图中移动实际上是否移动项目并在控制台打印Moving between collection views...

4. Adding a Placeholder

从外部应用程序获取项目并将其加载到目标应用程序中可能需要一些时间。 向用户提供视觉反馈(例如显示占位符)是一种很好的做法。

用以下代码替换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)
      })
    }
  }
}

这是正在发生的事情:

  • 1) 为新内容创建占位符单元格。
  • 2) 定义配置占位符单元格的block
  • 3) 将占位符插入集合视图中。
  • 4) 提交插入以将占位符与最终单元格交换。

构建并运行应用程序。 从Reminders中拖放项目。 将项目放入集合视图时,请注意占位符文本的简要外观:


Multiple Data Representations

您可以配置可以传递到目标应用程序或从源应用程序使用的数据类型。

使用init(object :)创建项目提供程序item provider时,传入的对象必须符合NSItemProviderWriting。 采用该协议包括为您可以导出的数据指定统一类型标识符(uniform type identifiers - UTI)并处理每个数据表示的导出。

例如,您可能希望为仅接受字符串的应用导出geocache的字符串表示形式。 或者您可能想要导出照片应用的图像表示。 对于您控制下使用地理位置的应用,您可能希望导出完整的数据模型。

要正确使用已dropped的项并将其转换为geocaches,您的数据模型应采用NSItemProviderReading。 然后,您可以实现协议方法以指定可以使用的数据表示形式。 您还将实现它们以指定如何根据源应用程序发送的内容强制传入数据。

到目前为止,在应用程序之间拖放地理缓存时,您已经使用过字符串。 NSString自动支持NSItemProviderWritingNSItemProviderReading,因此您不必编写任何特殊代码。

要处理多种数据类型,您将更改geocache数据模型。 您可以在Geocache项目中找到它,它是您打开的Xcode工作区的一部分。

Geocache项目中,打开Geocache.swift并在Foundation import后添加以下内容:

import MobileCoreServices

您需要此框架来使用预定义的UTI,例如代表PNGUTI

在上次导入后立即添加以下内容:

public let geocacheTypeId = "com.xxxxx.geocache" 

您创建一个代表geocache的自定义字符串标识符。

1. Reading and Writing Geocaches

将以下扩展名添加到文件末尾:

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并执行以下操作:

  • 1) 指定可以传送到目标应用程序的数据表示。 您希望返回从对象的最高保真度版本到最低值排序的字符串数组。
  • 2) 实现在请求时将数据传递到目标应用程序的方法。 系统在dropped项目时会调用此方法并传入适当的类型标识符。
  • 3) 如果传入PNG标识符,则在完成处理程序中返回geocache的图像。
  • 4) 如果传入文本标识符,则在完成处理程序中返回geocache的名称。
  • 5) 如果传入自定义geocache类型标识符,则返回与整个geocache对应的数据对象。

现在,在分配geocacheTypeId后立即添加以下枚举:

enum EncodingError: Error {
  case invalidData
}

当读取数据时出现问题,您将使用它来返回错误代码。

接下来,将以下内容添加到文件末尾:

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以指定如何处理传入数据。 这是正在做的事情:

  • 1) 指定模型可以使用的传入数据的类型。 此处列出的UTI代表geocache和文本。
  • 2) 给定类型标识符,实现导入数据required的协议方法。
  • 3) 对于文本标识符,使用基于传入文本和占位符信息的名称创建新的地理缓存。
  • 4) 对于geocache标识符,解码传入的数据并使用它来创建完整的geocache模型。

错误或无法识别的类型标识符会引发您之前定义的错误。

2. Back to My App

active scheme更改为Geocache并构建项目。 然后将active scheme更改回CacheMaker

CacheMaker中,转到CachesDataSource.swift并在dragItems(for:)内部将itemProvider赋值更改为:

let itemProvider = NSItemProvider(object: geocache) 

在这里,您可以使用geocache初始化项目提供程序,因为您的模型采用NSItemProviderWriting来正确导出数据。

打开CachesViewController.swift并找到collectionView(_:performDropWith :)。 在.copy case下,使用以下内容替换item providerloadObject调用:

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类型的对象。 完成块现在返回一个可以直接使用的geocache

构建并运行应用程序。 如有必要,将Reminders放在Split View中。 检查RemindersCacheMaker之间的拖放项是否像以前一样工作:

Split View中显示Photos以替换Reminders。 从正在进行的通道拖动地理缓存并将其放入Photos以验证您是否可以导出地理缓存的图像表示:

您可以使用临时hack测试完整数据模型导出路径。 转到CachesViewController.swift并在collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)中使用以下内容替换返回移动操作的行:

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

您在与复制操作相同的应用程序中配置拖放。 这会触发应导出和导入完整数据模型的代码。

构建并运行应用程序。 测试在应用程序中移动项目会生成geocache的正确副本:

collectionView(_:dropSessionDidUpdate:withDestinationIndexPath :)中恢复临时hack,以便应用程序内拖放执行移动操作:

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

构建并运行应用程序以恢复到pre-hack的状态。


Adding Drag Support to a Custom View

您已经了解了如何向集合视图添加拖放支持。 将此支持添加到table views遵循类似的过程。

您还可以向自定义视图添加拖放功能。 基本步骤包括:

  • 将交互对象添加到自定义视图。
  • 在交互委托中实现协议方法以提供或使用数据。

是时候介绍用于编辑地理藏宝的伴侣应用程序CacheEditor。 将active scheme更改为
CacheEditor。 构建并运行应用程序,然后将设备旋转到横向模式:

Split View中查看CacheMaker,将其放在CacheEditor的左侧。 调整Split View的大小,使两个应用程序占用大约一半的宽度:

尝试将geocacheCacheEditor拖到CacheMaker中。 我的朋友,你是一个令人沮丧的经历。

您将使用CacheEditor中的一个关键文件CacheDetailViewController.swift,它显示geocache详细信息。 打开该文件并将以下代码添加到最后:

// 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并实现拖动活动开始时调用的方法。 代码应该与您在CacheMaker中看到的类似。 您将带有geocache的拖动项目作为item provider返回。

在调用super之后立即将以下内容添加到viewDidLoad()

view.addInteraction(UIDragInteraction(delegate: self))  

在这里,您将创建与作为代理的视图控制器的拖动交互。 然后,您将交互添加到视图中。

构建并运行CacheEditor。 验证您现在可以从CacheEditor拖动地理缓存并将其放入CacheMaker

尝试将地理缓存从CacheMaker拖到CacheEditor中。 拖动开始时,它不会drop。 这是你的下一个任务。


Adding Drop Support to a Custom View

仍然在CacheDetailViewController.swift中,将以下内容添加到文件的末尾:

// 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并实现可选optional方法来跟踪和正确处理drops

第一种方法将drop限制为传入Geocache对象的应用程序。

第二种方法返回复制操作作为处理drop的建议方法。 当用户在drop交互视图上拖动项目时,将调用此方法。 虽然此协议方法是可选的,您也需要实现它以接受drops

drop手势完成时调用最后一个协议方法。 您从会话中获取item provider并启动数据提取。 然后,加载第一个geocache并更新视图。

接下来,在拖动交互设置之后将此代码添加到viewDidLoad()

view.addInteraction(UIDropInteraction(delegate: self))  

通过此操作,您可以创建drop交互并将视图控制器设置为委托。 然后,您将交互添加到视图中。

构建并运行应用程序。 验证您是否可以将geocache dropCacheEditor中:

只需很少的代码行,您就可以向自定义视图添加拖放支持。

恭喜! 您已使用拖放操作来使geocache管理示例应用程序正常运行。

您现在应该能够添加拖放功能,以增强许多应用内和跨应用体验了。

后记

本篇主要讲述了基于UICollectionViews和Drag-Drop在两个APP间的使用示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容