
由于拖放, iOS 11将iOS(特别是iPad)提升为真正的多任务平台。 这有望模糊应用程序之间的界限,使内容易于共享。 借助多点触摸功能,iOS 11可以自然直观地移动内容,从而使苹果的移动设备与其台式机和笔记本电脑用户所享有的丰富性更加接近。

期待已久的功能使您可以将项目拖到同一应用程序中的另一个位置或另一个应用程序中。 通过一个连续的手势,可以通过分屏布置或通过底座进行操作。 此外,用户不仅限于选择单个项目,还可以同时拖动多个项目。 许多应用程序,包括照片和文件等系统应用程序,都利用了多选和拖动多个文件的优势。

本教程的目标
本教程将向您介绍拖放操作,然后深入研究在基于表视图的应用程序中使用新的拖放SDK的体系结构和策略。 我想帮助像您这样的开发人员使您的应用程序适应新兴的UI行为,这在将来的iOS应用程序中将成为标准。
在本教程中,我们将介绍以下内容:
- 了解拖放
- 在表格视图中实现拖放
- 拖放最佳做法
在本教程的后半部分,我们将介绍一些实用的步骤,以使简单的表格视图应用程序能够利用拖放功能,首先是在Xcode中创建新项目时可用的Apple默认表格视图模板之一。 9.如果要继续,请继续并克隆教程的GitHub存储库 。
假设知识
本教程假定您具有iOS开发人员的经验,并已在Swift或Objective-C中使用过UIKit库(包括UITableView
,并且对委托和协议有所了解。
了解拖放
使用Apple的术语,将可视项从源位置拖动到目标位置。 这称为拖动活动,该活动发生在单个应用程序内(支持iPad和iPhone),或者跨两个应用程序(仅在iPad上可用)进行。
在进行拖动会话时,源应用程序和目标应用程序都仍处于活动状态并正常运行,从而支持用户交互。 实际上,与macOS不同,iOS通过使用多个手指来支持多个同时拖动活动。
但是,让我们专注于单个拖动项,以及它如何将promise作为其数据表示的合同。
拖动项目作为承诺
每个拖动项都可以看作是一个承诺,即包含的数据表示形式,它将从源拖放到其目的地。 拖动项目使用项目提供程序,并使用统一的类型标识符填充其registeredTypeIdentifiers
,这些类型标识符是将致力于与预览图像(视觉上固定在用户的触摸点下方)一起交付给预期目标的单个数据表示形式。下面:
拖动项是通过UIDragInteractionDelegate
从源位置构造的,并通过UIDragInteractionDelegate
在目标位置上进行UIDropInteractionDelegate
。 源位置必须符合NSItemProviderWriting
协议,而目标位置必须符合NSItemProviderReading
协议。
这是通过诺言将视图指定为拖动项的基本概述。 在建立放置目标之前,让我们看看如何从视图实现拖动源。
实施拖动源
将注意力集中在拖放的第一部分(拖动源)上,我们需要在用户启动拖动活动时完成以下步骤:
- 使视图符合
UIDragInterationDelegate
协议。 - 通过
dragInteraction(_:itemsForBeginning:)
声明将构成项目dragInteraction(_:itemsForBeginning:)
的数据项目。 - 用拖动项填充拖动会话,以准备用户将其拖动到放置目标。

您需要做的第一件事是,通过创建一个新的UIDragInteraction
实例并将其与ViewController
的视图的addInteraction
属性及其委托相关联,使指定的视图符合UIDragInterationDelegate
协议,如下所示:
let dragInteraction = UIDragInteraction(delegate: dragInteractionDelegate)
view.addInteraction(dragInteraction)
声明拖动源之后,您可以通过实现委托方法dragInteraction(_:itemsForBeginning:)
来创建一个拖动项,本质上是一个数据表示的承诺,系统将调用该方法返回一个或多个拖动项的数组以进行填充拖动会话的items属性。 以下示例在返回数据项数组之前,根据图像承诺创建一个NSItemProvider
:
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let imagePromise = imageView.image else {
return [] //By returning an empty array you disable dragging.
}
let provider = NSItemProvider(object: imagePromise)
let item = UIDragItem(itemProvider: provider)
return [item]
}
上面的委托方法响应于用户开始拖动项目时触发的拖动请求,并且手势识别器( UIGestureRecognizer
)将“拖动开始”消息发送回系统。 这实际上是初始化“拖动会话”的原因。
接下来,我们继续实现放置目标,以处理会话中启动的拖动项数组。
实施放置目标
同样,要使您的指定视图接受和使用数据作为放置目标的一部分,您将需要完成以下步骤:
- 实例化一个
DropInteraction
。 - 使用
dropInteraction(_:canHandle:)
声明您将接受的数据项类型(如果有)。 - 使用
dropInteraction(_:sessionDidUpdate:)
协议方法实施删除建议,说明您将要复制,移动,拒绝还是取消会话。 - 最后,使用
dropInteraction(_:performDrop:)
协议方法使用数据项。

正如我们将视图配置为启用拖动一样,我们将使用UIDropinteractionDelegate
并实现其DropInteraction
委托方法,将提名视图对称地配置为接受拖动会话中的拖放项目:
let dropInteraction = UIDropInteraction(delegate: dropInteractionDelegate)
view.addInteraction(dropInteraction)
为了指定View是否可以接受拖动项或拒绝它,我们实现了dropInteraction(_:canHandle:)
协议方法。 以下方法通过说明其可以接收的对象的类型(在本例中为UIImages),使我们的视图告诉系统它是否可以接受项目。
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
// Explicitly state the acceptable drop item type here
return session.canLoadObjects(ofClass: UIImage.self)
}
如果视图对象不接受任何放置交互,则应从此方法返回false。
接下来,绑定一个放置建议,以便从放置会话接受数据。 尽管这是可选方法,但强烈建议您实施此方法,因为它提供了有关放置是否会导致复制项目,移动它或是否完全拒绝放置的视觉提示。 通过实现dropInteraction(_:sessionDidUpdate:)
协议方法(该方法返回UIDropProposal
,可以使用特定的操作枚举类型( UIDropOperation
)指示提议类型。 您可以返回的有效类型是:
-
cancel
-
forbidden
-
copy
-
move
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
// Signal to the system that you will move the item from the source app (you could also state .copy to copy as opposed to move)
return UIDropProposal(operation: .move)
}
最后,要消耗目标位置中的数据项内容,请在后台队列(而不是在主队列中,这确保响应性)中实现dropInteraction(_:performDrop:)
协议方法。 如下图所示:
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
// Consume UIImage drag items
session.loadObjects(ofClass: UIImage.self) { items in
let images = items as! [UIImage]
self.imageView.image = images.first
}
}
我们已经演示了如何在自定义视图中实现拖放,因此现在让我们继续本教程的实际部分,并在具有表视图的应用程序中实现拖放。
在表格视图中实现拖放
到目前为止,我们一直在讨论如何在自定义视图中实现拖放操作,但是Apple也通过拖放操作轻松地增强了表格视图和集合视图。 文本字段和视图自动支持开箱即用,而表和集合视图则公开了用于自定义其拖放行为的特定方法,委托和属性。 我们将在短期内介绍一下。
首先在Xcode 9中创建一个新项目,确保您从模板窗口中选择Master-Detail App :

在开始执行其余步骤之前,请继续构建并运行该项目并进行一些尝试。 选择加号( + )按钮时,您会看到它生成一个新的时间戳记日期。 我们将通过允许用户拖动和排序时间戳来改进此应用程序。
通过使我们的表视图符合UITableViewDragDelegate
和UITableViewDropDelegate
协议,通过专用API(支持集合中的拖放),通过专用API在表视图(以及集合)中支持拖放。 打开MasterViewController.swift文件,并将以下内容添加到viewDidLoad()
方法中:
override func viewDidLoad() {
super.viewDidLoad()
...
self.tableView.dragDelegate = self
self.tableView.dropDelegate = self
...
与自定义视图一样,当用户拖动选定的行或多行/多选时,我们需要处理新的拖动会话。 我们使用委托方法tableView(_:itemsForBeginning:at:)
。 在此方法中,您将返回一个开始拖动选定行的填充数组,或者返回一个空数组以防止用户从该特定索引路径拖动内容。
将以下方法添加到您的MasterViewController.swift文件中:
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let dateItem = self.objects[indexPath.row] as! String
let data = dateItem.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
completion(data, nil)
return nil
}
return [
UIDragItem(itemProvider: itemProvider)
]
}
在上一节中,您应该已经熟悉了所添加的一些代码,但是实质上,我们要做的是从所选对象创建数据项,将其包装在NSItemProvider
,然后将其返回到DragItem
。
将注意力转移到启用放置会话上,继续添加以下两种方法:
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: NSString.self)
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.hasActiveDrag {
if session.items.count > 1 {
return UITableViewDropProposal(operation: .cancel)
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
第一种方法告诉系统它可以在其放置会话中处理String数据类型。 第二个委托方法tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
跟踪潜在的放置位置,并在每次更改时通知该方法。 它还使用小视觉图标提示显示视觉反馈,以使用户知道特定位置是禁止还是可接受。
最后,我们处理删除并使用数据项,调用tableView(_:performDropWith:)
,获取拖动的数据项行,更新表视图的数据源,并将必要的行插入表中。
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// Get last index path of table view.
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: NSString.self) { items in
// Consume drag items.
let stringItems = items as! [String]
var indexPaths = [IndexPath]()
for (index, item) in stringItems.enumerated() {
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
self.objects.insert(item, at: indexPath.row)
indexPaths.append(indexPath)
}
tableView.insertRows(at: indexPaths, with: .automatic)
}
}
进一步阅读
有关在表格视图中支持拖放的更多信息,请参阅Apple自己的开发人员文档,有关在表格视图中支持拖放 。
拖放最佳做法
我们涵盖的内容应帮助您在应用程序中实现拖放操作,使用户可以在其现有应用程序内以及跨应用程序的内容之间以视觉和交互方式移动内容。
但是,除了具有有关如何实现拖放的技术知识外,您还必须花时间考虑如何按照Apple在其人机界面指南 (HIG)中倡导的最佳实践来实现拖放。以便为用户提供最佳的iOS 11用户体验。
最后,我们将从视觉提示入手,考虑一些最重要的方面。 根据HIG的介绍,拖放的基本经验是,当用户与某些内容进行交互时,视觉提示会向用户指示活动的拖动会话(以使内容元素上升表示),以及标志以指示何时拖放还是不可能的。

当我们包含tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
方法时,我们已经在较早的示例中使用了这种最佳实践,说明放置目的地是移动,复制还是禁止。 您应该通过自定义视图和交互来确保维持其他iOS 11应用程序(尤其是系统应用程序)支持的预期行为集。
要考虑的另一个重要方面是确定拖动会话将导致移动还是复制。 作为一般经验法则,Apple建议您在同一应用程序中工作时通常会导致移动,而在不同应用程序之间拖动时复制数据项更有意义。 当然,尽管有例外,但基本原则是它应该对用户有意义,并且他们期望发生什么。
您还应该考虑源和目的地,以及是否拖动某些东西是否有意义。
让我们看一下苹果自己的一些系统实用程序。 例如,Notes允许您通过拆分屏幕选择文本内容并将其拖到应用程序内的其他位置,或跨iPad拖到iPad上的其他应用程序。 提醒应用程序允许您将提醒项从一个列表移动到另一列表。 在决定用户如何使用您的内容时,请考虑功能性。
苹果公司的指导方针是,除了支持复制和粘贴这些类型的元素外,所有可编辑的内容都应支持接受放置的内容,而任何可选择的内容都应接受可拖动的内容。 通过利用标准的系统文本视图和文本字段,您将获得对拖放操作的支持。
您还应该支持多项目拖放,而不是仅支持单个项目,用户可以使用多个手指同时选择多个项目,将所选项目堆叠到一组中以放入其预期的目的地。 一个实际的例子是在“照片”应用程序中选择多张图片,或在“文件”应用程序中选择多个文件。
最终指南是为用户提供撤消操作或“撤消”操作的功能。 用户已经很长时间习惯了在大多数流行的应用程序中撤消操作的概念,并且拖放也不例外。 用户应该有信心能够启动拖放操作,并且如果将元素放置在错误的目的地也能够撤消该操作。
进一步阅读
除了我们已经介绍的内容之外,还有更多的拖放最佳实践指南,包括如何支持拖放的视觉指示器提示,显示失败的拖放动作以及非即时拖动会话的进度指示器,例如数据传输。 有关最佳做法的完整列表,请参考Apple的iOS iOS人机界面指南 。
结论
在本教程中,您已经学会了如何通过iOS 11的拖放功能来丰富iOS应用程序。在此过程中,我们探索了如何同时启用自定义视图和表格视图作为拖动源和放置目标。
作为iOS向手势驱动型用户界面发展的一部分,毫无疑问,拖放将Swift成为系统范围内用户的预期功能,因此,所有第三方应用程序也都应遵循该规范。 与实现拖放同样重要,您将需要正确地实现它,以使其成为用户的第二天性,包括简单性和功能性。