史上第二走心的 iOS11 Drag Drop 教程

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!

史上第二走心的 iOS11 Drag Drop 教程

史上第二走心的 iOS11 Drag Drop 教程


话不多说,先上效果图


史上第二走心的 iOS11 Drag Drop 教程

普通view拖拽效果


史上第二走心的 iOS11 Drag Drop 教程

TableView拖拽效果


史上第二走心的 iOS11 Drag Drop 教程

CollectionView效果


史上第二走心的 iOS11 Drag Drop 教程

muti-touch效果


史上第二走心的 iOS11 Drag Drop 教程

多app交互


世界上最大的男性交友网站有demo


一.Tips:你必须要知道的概念


1. Drag 和 Drop 是什么呢?


  • 一种以图形展现的方式把数据从一个 app 移动或拷贝到另一个 app(仅限iPad),或者在程序内部进行

  • 充分利用了 iOS11 中新的文件系统,只有在请求数据的时候才会去移动数据,而且保证只传输需要的数据

  • 通过异步的方式进行传输,这样就不会阻塞runloop,从而保证在传输数据的时候用户也有一个顺畅的交互体验


史上第二走心的 iOS11 Drag Drop 教程

drag和drop的基本交互图和支持的控件


2. 安全性:


  • 拖拽复制的过程不像剪切板那样,而是保证数据只对目标app可见

  • 提供数据源的app可以限制本身的数据源只可在本 app 或者 公司组app 之间有权限使用,当然也可以开放于所有 app,也支持企业用户的管理配置


3. dragSession 的过程


  • Lift:用户长按 item,item 脱离屏幕

  • Drag :用户开始拖拽,此时可进行 自定义视图预览、添加其他item添加内容、悬停进行导航(即iPad 中打开别的app)

  • Set Down :此时用户无非想进行两种操作:取消拖拽 或者 在当前手指离开的位置对 item 进行 drop 操作

  • Data Transfer :目标app 会向 源app 进行数据请求

  • 这些都是围绕交互这一概念构造的:即类似手势识别器的概念,接收到用户的操作后,进行view层级的改变


史上第二走心的 iOS11 Drag Drop 教程


4. Others


  • 需要给用户提供 muti-touch 的使用,这一点也是为了支持企业用户的管理配置(比如一个手指选中一段文字,长按其处于lifting状态,另外一个手指选中若干张图片,然后打开邮件,把文字和图片放进邮件,视觉反馈是及时的,动画效果也很棒)


史上第二走心的 iOS11 Drag Drop 教程

iPad 可实现的功能还是很丰富的


二、以CollectionView 为例,讲一下整个拖拽的api使用情况


在API设计方面,分为两个步骤:Drag 和 Drop,对应着两套协议 UICollectionViewDragDelegate

UICollectionViewDropDelegate,因此在创建 CollectionView 的时候要增加以下代码:


- (void)buildCollectionView {

    _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:flowLayout];

    [_collectionView registerClass:[WPFImageCollectionViewCell class] forCellWithReuseIdentifier:imageCellIdentifier];

    _collectionView.delegate = self;

    _collectionView.dataSource = self;

    // 设置代理对象

    _collectionView.dragDelegate = self;

    _collectionView.dropDelegate = self;


    _collectionView.dragInteractionEnabled = YES;

    _collectionView.reorderingCadence = UICollectionViewReorderingCadenceImmediate;

    _collectionView.springLoaded = YES;

    _collectionView.backgroundColor = [UIColor whiteColor];

}


1. 创建CollectionView注意点总结:


  • dragInteractionEnabled 属性在 iPad 上默认是YES,在 iPhone 默认是 NO,只有设置为 YES 才可以进行 drag 操作

  • reorderingCadence (重排序节奏)可以调节集合视图重排序的响应性。 是 CollectionView 独有的属性(相对于UITableView),因为 其独有的二维网格的布局,因此在重新排序的过程中有时候会发生元素回流了,有时候只是移动到别的位置,不想要这样的效果,就可以修改这个属性改变其相应性


  • UICollectionViewReorderingCadenceImmediate:默认值,当开始移动的时候就立即回流集合视图布局,可以理解为实时的重新排序

  • UICollectionViewReorderingCadenceFast:如果你快速移动,CollectionView 不会立即重新布局,只有在停止移动的时候才会重新布局

  • UICollectionViewReorderingCadenceSlow:停止移动再过一会儿才会开始回流,重新布局


  • springLoaded :弹簧加载是一种导航和**控件的方式,在整个系统中,当处于 dragSession 的时候,只要悬浮在cell上面,就会高亮,然后就会**


  • UITableView 和 UICollectionView 都可以使用该方式加载,因为他们都遵守 UISpringLoadedInteractionSupporting 协议

  • 当用户在单元格使用弹性加载时,我们要选择 CollectionView 或tableView 中的 item 或cell

  • 使用-(BOOL)collectionView:shouldSpringLoadItemAtIndexPath:withContext:来自定义也是可以的


  • collectionView:itemsForAddingToDragSession: atIndexPath: :该方法是muti-touch对应的方法


  • 当接收到添加item响应时,会调用该方法向已经存在的drag会话中添加item

  • 如果需要,可以使用提供的点(在集合视图的坐标空间中)进行其他命中测试。

  • 如果该方法未实现,或返回空数组,则不会将任何 item 添加到拖动,手势也会正常的响应



- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView itemsForAddingToDragSession:(id<UIDragSession>)session atIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point {

    NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];

    UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];

    return @[item];

}


史上第二走心的 iOS11 Drag Drop 教程

再放一遍这个效果图


2. UICollectionViewDragDelegate(初始化和自定义拖动方法)


  • collectionView: itemsForBeginningDragSession:atIndexPath:提供一个 给定 indexPath 的可进行 drag 操作的 item(类似 hitTest: 方法周到该响应的view )如果返回 nil,则不会发生任何拖拽事件



由于是返回一个数组,因此可以根据自己的需求来实现该方法:比如拖拽一个item,就可以把该组的所有 item 放进 dragSession 中,右上角会有小蓝圈圈显示个数(但是这种情况下要对数组进行重新排序,因为数组中的最后一个元素会成为Lift 操作中的最上面的一个元素,排序后可以让最先进入dragSession的item放在lift效果的最前面)


- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id<UIDragSession>)session atIndexPath:(NSIndexPath *)indexPath {

    

    NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];

    UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];

    self.dragIndexPath = indexPath;

    return @[item];

}



史上第二走心的 iOS11 Drag Drop 教程

  • collectionView:dragPreviewParametersForItemAtIndexPath:允许对从取消或返回到 CollectionView 的 item 使用自定义预览,如果该方法没有实现或者返回nil,那么整个 cell 将用于预览


  • UIDragPreviewParameters 有两个属性:


  • backgroundColor 设置背景颜色,因为有的视图本身就是半透明的,添加背景色视觉效果更好

  • visiblePath设置视图的可见区域,比如可以自定义为圆角矩形或图中的某一块区域等,但是要注意裁剪的Rect 在目标视图中必须要有意义;该属性也要标记一下center方便进行定位



史上第二走心的 iOS11 Drag Drop 教程

裁剪图中的某一块区域



史上第二走心的 iOS11 Drag Drop 教程

选取的区域也可以大于这张图,实现添加相框的效果


史上第二走心的 iOS11 Drag Drop 教程

再高级的功能可以实现目标区域内添加多个rect到dragSession


- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dragPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath {

    // 可以在该方法内使用 贝塞尔曲线 对单元格的一个具体区域进行裁剪

    UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];

    

    CGFloat previewLength = self.flowLayout.itemSize.width;

    CGRect rect = CGRectMake(0, 0, previewLength, previewLength);

    parameters.visiblePath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:5];

    parameters.backgroundColor = [UIColor clearColor];

    return parameters;

}


  • 还有一些对于 drag 生命周期对应的回调方法,可以在这些方法里添加各种动画效果


/* 当 lift animation 完成之后开始拖拽之前会调用该方法

 * 该方法肯定会对应着 -collectionView:dragSessionDidEnd: 的调用

 */

- (void)collectionView:(UICollectionView *)collectionView dragSessionWillBegin:(id<UIDragSession>)session {

    NSLog(@"dragSessionWillBegin --> drag 会话将要开始");

}


// 拖拽结束的时候会调用该方法

- (void)collectionView:(UICollectionView *)collectionView dragSessionDidEnd:(id<UIDragSession>)session {

    NSLog(@"dragSessionDidEnd --> drag 会话已经结束");

}


当然也可以在这些方法里面设置自定义的dragPreview,比如 iPad 中原生的通讯图、地图所展现的功能



史上第二走心的 iOS11 Drag Drop 教程

在 dragSessionWillBegin 方法里面自定义 preview 视图


3. UICollectionViewDropDelegate(迁移数据和自定义释放动画)


史上第二走心的 iOS11 Drag Drop 教程

Drop手势的流程图


  • collectionView:performDropWithCoordinator: 方法使用 dropCoordinator 去置顶如果处理当前 drop 会话的item 到指定的最终位置, 同时也会根据drop item返回的数据更新数据源


  • 当用户开始进行 drop 操作的时候会调用这个方法

  • 如果该方法不做任何事,将会执行默认的动画

  • 注意:只有在这个方法中才可以请求到数据

  • 请求的方式是异步的,因此不要阻止数据的传输,如果阻止时间过长,就不清楚数据要多久才能到达,系统甚至可能会kill掉你的应用


- (void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {

    

    NSIndexPath *destinationIndexPath = coordinator.destinationIndexPath;

    UIDragItem *dragItem = coordinator.items.firstObject.dragItem;

    UIImage *image = self.dataSource[self.dragIndexPath.row];

    // 如果开始拖拽的 indexPath 和 要释放的目标 indexPath 一致,就不做处理

    if (self.dragIndexPath.section == destinationIndexPath.section && self.dragIndexPath.row == destinationIndexPath.row) {

        return;

    }

    

    // 更新 CollectionView

    [collectionView performBatchUpdates:^{

        // 目标 cell 换位置

        [self.dataSource removeObjectAtIndex:self.dragIndexPath.item];

        [self.dataSource insertObject:image atIndex:destinationIndexPath.item];

        

        [collectionView moveItemAtIndexPath:self.dragIndexPath toIndexPath:destinationIndexPath];

    } completion:^(BOOL finished) {

        

    }];

    

    [coordinator dropItem:dragItem toItemAtIndexPath:destinationIndexPath];

}


  • collectionView: dropSessionDidUpdate: withDestinationIndexPath: 该方法是提供释放方案的方法,虽然是optional,但是最好实现


  • 当 跟踪 drop 行为在 tableView 空间坐标区域内部时会频繁调用(因此要尽量减少这个方法的工作量,否则帧率就会降低)

  • 当drop手势在某个section末端的时候,传递的目标索引路径还不存在(此时 indexPath 等于 该 section 的行数),这时候会追加到该section 的末尾

  • 在某些情况下,目标索引路径可能为空(比如拖到一个没有cell的空白区域)

  • 请注意,在某些情况下,你的建议可能不被系统所允许,此时系统将执行不同的建议

  • 你可以通过 -[session locationInView:] 做你自己的命中测试

  • UICollectionViewDropIntent对应的三个枚举值


  • UICollectionViewDropIntentUnspecified 将会接收drop,但是具体的位置要稍后才能确定;不会开启一个缺口,可以通过添加视觉效果给用户传达这一信息

  • UICollectionViewDropIntentInsertAtDestinationIndexPathdrop将会被插入到目标索引中;将会打开一个缺口,模拟最后释放后的布局

  • UICollectionViewDropIntentInsertIntoDestinationIndexPathdrop 将会释放在目标索引路径,比如该cell是一个容器(集合),此时不会像 ?? 那个属性一样打开缺口,但是该条目标索引对应的cell会高亮显示

  • 补充:UITableView 在以上对应枚举值基础上,还有一个特有的 automatic 属性,可以自动判断是放入文件夹还是打开缺口进入目标索引


  • UIDropOperation对应的四种状态。第四种 forbidden 是不允许在当前位置drop:比如要把一个图片放在一个文件夹内,但是这个文件夹是只读的,就会出现这个图标


史上第二走心的 iOS11 Drag Drop 教程


- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView dropSessionDidUpdate:(id<UIDropSession>)session withDestinationIndexPath:(nullable NSIndexPath *)destinationIndexPath {

    UICollectionViewDropProposal *dropProposal;

    // 如果是另外一个app,localDragSession为nil,此时就要执行copy,通过这个属性判断是否是在当前app中释放,当然只有 iPad 才需要这个适配

    if (session.localDragSession) {

        dropProposal = [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationCopy intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];

    } else {

        dropProposal = [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationCopy intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];

    }

    return dropProposal;

}


  • collectionView:canHandleDropSession:通过该方法判断对应的item 能否被 执行drop会话


  • 如果返回 NO,将不会调用接下来的代理方法

  • 如果没有实现该方法,那么默认返回 YES

- (BOOL)collectionView:(UICollectionView *)collectionView canHandleDropSession:(id<UIDropSession>)session {

    // 假设在该 drop 只能在当前本 app中可执行,在别的 app 中不可以

    if (session.localDragSession == nil) {

        return NO;

    }

    return YES;

}


  • collectionView: dropPreviewParametersForItemAtIndexPath: 当 item 执行drop 操作的时候,可以自定义预览图


  • 如果没有实现该方法或者返回nil,整个cell将会被用于预览图

  • 该方法会经由  -[UICollectionViewDropCoordinator dropItem:toItemAtIndexPath:]调用

  • 如果要去自定义占位drop,可以查看 UICollectionViewDropPlaceholder.previewParametersProvider


- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dropPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath {


    return nil;

}


  • 当然还有一些 常规的 drop 过程回调的方法


/* 当drop会话进入到 collectionView 的坐标区域内就会调用,

 * 早于- [collectionView dragSessionWillBegin] 调用

 */

- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnter:(id<UIDropSession>)session {

    NSLog(@"dropSessionDidEnter --> dropSession进入目标区域");

}


/* 当 dropSession 不在collectionView 目标区域的时候会被调用

 */

- (void)collectionView:(UICollectionView *)collectionView dropSessionDidExit:(id<UIDropSession>)session {

    NSLog(@"dropSessionDidExit --> dropSession 离开目标区域");

}


/* 当dropSession 完成时会被调用,不管结果如何

 * 适合在这个方法里做一些清理的操作

 */

- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnd:(id<UIDropSession>)session {

    NSLog(@"dropSessionDidEnd --> dropSession 已完成");

}


4. 优化


  • 涉及到app间拖动的时候,比如把相册中照片拖到邮件中,为什么相册中的小尺寸到了邮件中就刚刚和邮件中textView 宽度一致呢?


  • 在方法collectionView:itemsForBeginningDragSession: atIndexPath: 中,通过设置itemProvider.preferredPresentationSize 来设置item执行 drop 时的期望大小。这样 邮件app 在后台就能读取到这个尺寸大小,从而正常地显示


5. Placeholder


这个在demo里没写,因为只有iPad才支持 app 间传递数据,我想史上第一走心的教程一定会详细讲述这个方法的


史上第二走心的 iOS11 Drag Drop 教程

由于loadObject是异步的,因此加载数据和显示preview是两条不同的时间线


  • 使用场景:拖拽的item需要从服务器下载,比如拖拽相册中存储在iCloud 中的照片至邮件app中,就要先从 iCloud 下载,再进行下一步的展示,因此可能要等待一段时间才能下载完成,而且下载多个item还可能是乱序到达的。此时就需要PlaceHolder进行

  • 异步加载数据的时候可以用 PlaceHolder 推迟更新数据源直到数据加载完毕,从而保证UI 完全的响应性,不至于让用户长时间面对一个白板等待数据的传输

  • 如何创建PlaceHolder?通过释放协调器dropCoordinator来创建,从而将其插入到占位符中,并添加动画

  • 使用PlaceHolder 注意事项:(app间拖拽的时候,从A app 拖拽到 B app,确定位置之后,B中还未获取到数据,加载数据的过程中展示占位动画)


- (id<UICollectionViewDropPlaceholderContext>)dropItem:(UIDragItem *)dragItem toPlaceholder:(UICollectionViewDropPlaceholder*)placeholder;


  1. 不要使用 reloadData,使用 performBatchUpdates: 来替代(因为 reloadData 会重设一切,删除一切 PlaceHolder)

  2. 可以使用 collectionView.hasUncommittedUpdates 来判断当前 CollectionView 是否还存在 PlaceHolder


6.数据传输(iPhone 开发者了解概念即可)


  • 所有的数据加载都是通过拖放实现的,NSITemProvider可以为你提供数据传输的进度和取消操作

  • 提供数据:


// 创建一个 NSItemProvider 对象,传递一个适用的对象

UIImage *image = [UIImage imageNamed:@"photo"];

NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:image];


  • 接收数据


  • loadObjectOfClass 返回一个progress

  • 调用一次loadObjectOfClass 只会返回一个特定的progress,通过KVO监听 UIDropSession.progress可以获得所有的进度

  • demo是针对 iPhone 开发的,因此没有具体实现


// 该方法中加载数据的方式是异步的,

NSProgress *progress = [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^(id<NSItemProviderReading>  _Nullable object, NSError * _Nullable error) {

#waning 该回调在一个非主队列进行,如果更新UI要回到主线程

        UIImage *image = (UIImage *)object;

        // 使用image

    }];

// 是否完成

BOOL isFinished = progress.isFinished;

// 当前已完成进度

CGFloat progressSoFar = progress.fractionCompleted;

    

[progress cancel];


  • 注册支持的文件类型ID的时候,最好具体到特定的类型,比如最好使用“public.png”代替“public.image”,“public.utf8-plain-text”代替“public.plain-text”,当然如果是仅支持公司内部特定的app间传递,也可以完全自定义


  • 新概念:数据编组(Data Marshaling)


  • 提供数据有三种方式:


  • 直接提供NSData:itemProvider.registerDataRepresentation(...)

  • 提供一个文件或者文件夹:itemProvider.registerFileRepresentation(...fileOptions:[])

  • 作为 File Provider 的引用:itemProvider.registerFileRepresentation(...fileOptions:[.openInPlace])


  • 接收数据也有三种方式:


  • 直接拷贝出NSData 的副本:itemProvider.loadDataRepresentation(...)

  • 将文件或文件夹拷贝到自己的容器内:itemProvider.loadDataRepresentation(...)

  • 尝试在本地打开文件:itemProvider.loadInPlaceFileRepresentation(...)


  • 数据编组直接做好了数据的转换:


  • 提供者想要提供一个 NSData 类型数据,数据编组就直接将这个数据写入文件并提供url的副本

  • 如果提供者提供的是文件夹,然后数据编组就会把文件压缩并提供NSData


  • 最后稍微提到了File的另一个主题,也就是文件系统的拖拽,在这里大概叙述一下:


  • 文件的拖拽可以设置三种权限


  • 对所有人可见

  • 同一个 team 可见

  • 仅对自己可见


  • 文件的拖拽有两种选项:


  • 直接提供副本

  • 提供url(意味着多个app可以共享一个文件),对方修改,本地可以看到修改的地方


三. UIView-Tips


UITableView 的api使用基本和 UICollectionView 一致,在此不再赘述,但是以下UIView的特性还要再强调以下


  • iPhone 项目上,在对view添加UIDragInteraction操作时,一定要设置其enable 属性为YES,否则不会响应drag操作(iPhone默认为NO,iPad默认为YES)


  • UIDropProposal的属性precise,如果设置为YES,视图的点击测试区域将略高于用户触摸位置,这能够在视图中进行更精确的放入,具体效果请看下图


  • 当然如果使用这个属性的话要在 targetPoint 添加一些UI的提示,给用户确切的反馈


史上第二走心的 iOS11 Drag Drop 教程

这样就能精准地放入文本中的特定位置


  • prefersFullSizePreview,默认情况下预览图都是等比例缩小的,因为过大是没有意义的,遮挡屏幕就会影响到用户交互,难以进行导航,但是有些时候也需要全尺寸的预览图(比如一个列表中需要重新布局,此时将整个列表缩小是没有意义的)


  • 但是有些情况下,系统始终会进行比例缩小,即使是设置了全尺寸预览


  • 组合拖动:如果添加多个项目进行拖动

  • 如果将item拖动到另外一个app,也肯定会等比例缩小


  • [itemProvider loadObjectOfClass: completionHandler:]


  • 该方法回调默认在主线程

  • 该方法返回一个progress,汇报加载的进度

  • 返回值 NSProgress 可以设置属性 cancelled和^cancellationHandler,也可以进行断点续传操作,因为数据传输可能需要很久,需要给用户取消的权利

  • 如果不想要显示这个进度,可以通过session.progressIndicatorStyle = UIDropSessionProgressIndicatorStyleNone; 来隐藏进度视图。

  • 也可以通过KVO监听progress实现自定义进度展示


史上第二走心的 iOS11 Drag Drop 教程

方法控制效果


文章有点长,感谢您的阅读。

demo地址(https://github.com/PengfeiWang666/iOS11-NewFeature)



史上第二走心的 iOS11 Drag Drop 教程

  • 作者:si1ence

  • 链接:http://www.jianshu.com/p/92d21cc6de99?utm_source=desktop&utm_medium=timeline

  • iOS开发整理发布,转载请联系作者授权

史上第二走心的 iOS11 Drag Drop 教程

史上第二走心的 iOS11 Drag Drop 教程【点击成为安卓大神】