版本记录
版本号 | 时间 |
---|---|
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
自动支持NSItemProviderWriting
和NSItemProviderReading
,因此您不必编写任何特殊代码。
要处理多种数据类型,您将更改geocache
数据模型。 您可以在Geocache
项目中找到它,它是您打开的Xcode
工作区的一部分。
在Geocache
项目中,打开Geocache.swift
并在Foundation import
后添加以下内容:
import MobileCoreServices
您需要此框架来使用预定义的UTI
,例如代表PNG
的UTI
。
在上次导入后立即添加以下内容:
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 provider
的loadObject
调用:
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
中。 检查Reminders
和CacheMaker
之间的拖放项是否像以前一样工作:
在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
的大小,使两个应用程序占用大约一半的宽度:
尝试将geocache
从CacheEditor
拖到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
drop
到CacheEditor
中:
只需很少的代码行,您就可以向自定义视图添加拖放支持。
恭喜! 您已使用拖放操作来使geocache
管理示例应用程序正常运行。
您现在应该能够添加拖放功能,以增强许多应用内和跨应用体验了。
后记
本篇主要讲述了基于UICollectionViews和Drag-Drop在两个APP间的使用示例,感兴趣的给个赞或者关注~~~