4b34a8072f2f3246f2f4a0ad5f1cd6c6
Drag and Drop 深入探究

大家对于 Drag and Drop 的如何使用已经在 《Introducing Drag and Drop》 中有所体验,本文章会在此深入讨论 Drag and Drop 的开发, 诸如 生命周期, 动画,预览,技巧。首先要明白 Drag and Drop 是一种在 Source App 和 Destination App 之间(这俩 App 可以是同一个)传输数据的协议Drag and Drop 看起来很高大上,其实苹果已经将 API 极大的简化,按照拆分的思路, 你只需要关注用户输入事件,即可完成整个过程。 因为我们在实际操作的时候, 可以独立的来考虑 Drag 和 Drop,所以本文将会分两部分: Drag SourceDrop Destination

Drag Source

设置 View 可被 Drag

设置并实现一个 UIDragInteraction 实例的 UIDragInteractionDelegate 代理方法 ,并将这个 interaction 添加给 View ,就实现了 View 可以被 Drag 功能(这种 View 上长按就会可以拖走);UITextViewUITableViewUICollectionView 这三个类有它们各自专属的方法来创建 Drag Item
interaction 的代理一般可以是 ViewController 或 单独的类,但都需要遵循 UIDragInteraction 协议;如果代理是 Controller 这个操作一般在 ViewDidLoad()或者在 View 的 Init() 方法内完成,interaction初始化时必须设置好代理。类似这样:

func customEnableDragging(on view: UIView, dragInteractionDelegate: UIDragInteractionDelegate) {            
    let dragInteraction = UIDragInteraction(delegate: dragInteractionDelegate)
    view.addInteraction(dragInteraction)
}

Session 包括两种 UIDragSessionUIDropSession,但 Session 都可以被理解为一根手指所干的事;比如一根手指拖拽着几张图片,那么在 drag 的区域,连带这些操作和拖拽的图片就是 drag session ,在 drop 区域这跟手指和他拖动的图片以及其他操作就是 drop session。但 DragSession 只包含一个 localContext: Any? 的自定义上下文标识符。Drop Session 还包括 progressIndicatorStyle: UIDropSessionProgressIndicatorStyle和方法 func loadObjects(ofClass aClass: NSItemProviderReading.Type, completion: @escaping ([NSItemProviderReading]) -> Void) -> Progress

为 Drag 过程提供数据

Drag 操作时需要为 DragView 提供 dragItem 所需数据,这个数据就是在 Drag 过程中实际传输的数据,比如字符串、图片、文件等;通过dragInteraction(_:itemsForBeginning:)方法来实现,这是 UIDragInteractionDelegate 的代理方法,并且这个方法只接受 NSItemProvider,也就是说你必须要把你的数据进行封装;比如你在拖拽一段文字,跨 App 传输,需要将需要传输的 App ,需要将对象封装成 NSItemProvider ,(如果传输的类对象是 NSString,NSAttributedString,NSURL,UIColor,UIImage,你不需关心 NSItemProviderReadingNSItemProviderWriting协议,也就是封装传输时候声明出数据的类型,以便数据接收方能够正确的取出和加载数据;如果是拖拽的数据是自定义的类,需要在数据的类中自行实现这个协议,来声明好 Uniform Type Identifiers , 有关具体可以参照 UTI Reference
下面是传输一个字符串时候的实现:

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
    // 因为 NSItemProvider 只支持类,所以你需要把 String Cast to NSString
    let stringItemProvider = NSItemProvider(object: "Hello World" as NSString)
    return [
    UIDragItem(itemProvider: stringItemProvider)
    ]
}

如果一个拖拽的 View 所包含的内容是多张图片的组合或 文字和图片的组合,可以这么实现

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
    return imageViews.map { (imageView) -> UIDragItem in
    let itemProvider = NSItemProvider(object: imageView.image!)
        let item = UIDragItem(itemProvider: itemProvider)
        item.localObject = imageView
        return item
    }
}

Drag 时整个的 iOS 的处理逻辑和生命周期


Drag 时,长按事件被识别为 Drag 手势,iOS 系统在受到 drag 手势的消息后,iOS 会初始化一个 drag session ,回调 itemForBegining 的代理方法,接下来 iOS 会把整个 Drag 的事务处理通过 DragInteractionDelegate 来处理。也就是说只有 itemForBegining 是必须实现的,其他方法都有默认实现

Drag 过程常用 UIDragInteractionDelegate 方法

接下来介绍一些 DragInteractionDelegate 的一些代理方法。
首先介绍一个概念 LiftLift 指的是用户长按开始拖拽后,被拽的物体一般会跟随手指而移动,这些被拖拽的 items 跟随手指移动的状态就是 Lift ,就像被手指吸附抬起悬空一样;

开始

Drag 开始时,会调用 itemForBegining 代理方法,紧接着回调用 previewForLifting 方法来获取每个 itemUITargetedDragPreview,也就是 itemLift 后的预览样式,如果你希望使用默认的预览样式,不要实现这个代理方法;如果不需要任何抬起动画预览样式,返回 nil

添加

Lift 状态下,用第二根手指来点选其他可以 DragView,就可以将新的 dragItem 加入当前的 Drag Session,注意这是同一个 drag session,一般情况下Drag and Drop 过程中只需要一个drag session ,但是也支持多个 session,这个需要自己去创建, 这里不赘述。one drag interaction touch + one drag interaction delegate = one active drag session「一根手指的长按拖拽触摸 + 一个 drag interaction 代理 = 一个活跃的 drag session」

func dragInteraction(_ interaction: UIDragInteraction, itemsForAddingTo session: UIDragSession, withTouchAt point: CGPoint) -> [UIDragItem[] {]}

取消

在 Drag 过程中,用户的手指至少会有一根在屏幕上,如果用户所有的手指都离开了屏幕,iOS 会告知代理方法开始处理「取消」的相关代理方法
首先会调用方法来实现 Cancel 时候的动画,这个方法每个 drag item 都会单独调用一次,如果不实现,,如果使用系统默认动画,请不要实现这个方法,系统会有默认实现;

optional func dragInteraction(_ interaction: UIDragInteraction, item: UIDragItem, willAnimateCancelWith animator: UIDragAnimating)

最后,所有的 drag item 都进行过 cancel animation 后,系统会调用以下方法结束对应的 drag session

optional func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, didEndWith operation: UIDropOperation)

所有代理方法的简介

Drag 的执行
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem]
//Drag 的执行最先回调的代理方法,必须实现,返回了 drag item

optional func dragInteraction(_ interaction: UIDragInteraction, itemsForAddingTo session: UIDragSession, withTouchAt point: CGPoint) -> [UIDragItem]
//用户在同一个 drag session 中添加 drag item 的代理方法,比如用户一根手指拖动着一张图片时,点击其他图片,就会触发添加操作

optional func dragInteraction(_ interaction: UIDragInteraction, sessionForAddingItems sessions: [UIDragSession], withTouchAt point: CGPoint) -> UIDragSession?
//如果需要实现用户两根手指各自拖动各自的 drag item,实现这个方法,参数中包含每个 session 对应的触摸坐标
Drag 时的自定义动画
optional func dragInteraction(_ interaction: UIDragInteraction, willAnimateLiftWith animator: UIDragAnimating, session: UIDragSession) 
//长按之后使 drag item「悬空」时的动画效果,如果使用默认实现,请不要实现这个方法
optional func dragInteraction(_ interaction: UIDragInteraction, item: UIDragItem, willAnimateCancelWith animator: UIDragAnimating)
//用户如果在拖动过程中手指离开屏幕,表示取消,此方法自定义「取消」的动画,注意每个 item 均可自定义
监听 Drag 的进程
optional func dragInteraction(_ interaction: UIDragInteraction, sessionWillBegin session: UIDragSession)
//监听drag session 开始时的回调方法,如果需要在 Drag 时候准备一些操作比如打点,可以在这里实现

optional func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, willAdd items: [UIDragItem], for addingInteraction: UIDragInteraction)
//session 中「将要添加」 drag item 时的监听回调方法

optional func dragInteraction(_ interaction: UIDragInteraction, sessionDidMove session: UIDragSession)
//session 在移动时的监听回调方法,用户的一根手指往往就是一个 session ,在用户移动手指时,会频繁调用这个方法

optional func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, willEndWith operation: UIDropOperation)
//session 「将要结束」时的监听回调方法

optional func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, didEndWith operation: UIDropOperation)
//session 「已经结束」时的监听回调方法

optional func dragInteraction(_ interaction: UIDragInteraction, sessionDidTransferItems session: UIDragSession)
//如果用户执行了 Drop 操作,并且数据或对象异步「传输完毕」后会调用这个方法,需要注意是异步,所以 UI 操作注意线程
Drag 时的自定义预览
optional func dragInteraction(_ interaction: UIDragInteraction, previewForLifting item: UIDragItem, session: UIDragSession) -> UITargetedDragPreview?
//被「悬空」的 drag item 的预览效果,如需使用默认的预览效果,返回 nil

optional func dragInteraction(_ interaction: UIDragInteraction, previewForCancelling item: UIDragItem, withDefault defaultPreview: UITargetedDragPreview) -> UITargetedDragPreview?
//用户取消 drag 时,取消时的预览效果,如需使用默认动画效果返回 nil

optional func dragInteraction(_ interaction: UIDragInteraction, prefersFullSizePreviewsFor session: UIDragSession) -> Bool
//如果需要全尺寸、不压缩大小的预览时,返回 true 。一般在 drag 时使用100\*100 左右的预览,上面的预览方法实现时已定义大小,默认 false
Drag 操作的限制

```swift
optional func dragInteraction(_ interaction: UIDragInteraction, sessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool
//是限制 drag item 是否必须限制在 source App 内,不许拖到其他 App 内,如果需要这个限制返回 true ,默认 false 且不需实现

top Created with Sketch.