iOS11-DragAndDrop拖放教程

推荐,关注

1570756780.png

1509605169882104.gif

1509605220418351.gif

1509605277306855.gif

1509605290387719.gif

Demo地址

一、必备概念

1. 基本概念
  • 一种以图形展现的方式把数据从一个app移动或拷贝到另一个app(仅限iPad),或者在程序内部进行
  • 充分利用了iOS11中新的文件系统,只有在请求数据的时候才会去移动数据,而且保证只传输需要的数据
  • 通过异步的方式进行传输,这样就不会阻塞runloop,从而保证在传输数据的时候用户也有一个顺畅的交互体验
拖和下降的基本交互图和支持的控件
  1. 安全性
  • 拖拽复制的过程不像剪切板那样,而是保证数据只对目标应用程序可见
  • 提供数据源的应用程序可以限制本身的数据源只可在本应用程序或者公司组件应用程序或者应用程序之间有权限使用,当然也可以开放于所有应用程序,也支持企业用户的管理配置
  1. dragSession的过程
  • Lift:用户长按项目,脱离屏幕
  • 拖动:用户开始拖拽,此时可能进行自定义视图预览,添加其他项目添加内容,悬停进行导航(即iPad中打开别的应用程序)
  • Set Down:此时用户无非想进行两种操作:取消拖拽或者在当前手指离开的位置对item进行drop操作
  • 数据传输:目标app会向源应用进行数据请求
  • 这些都是围绕交互这一概念构造的:即类似手势识别器的概念,接收到用户的操作后,进行视图层级的改变


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

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

  • 在API设计方面,分为两个步骤:拖动和拖放,对应着两套协议UICollectionViewDragDelegate和UICollectionViewDropDelegate,因此在创建CollectionView的时候要增加以下代码
_collectionView.dragDelegate = self;
_collectionView.dropDelegate = self;
_collectionView.dragInteractionEnabled = YES;
_collectionView.reorderingCadence = UICollectionViewReorderingCadenceImmediate;
    _collectionView.springLoaded = YES
  1. 创建CollectionView注意点总结
  • dragInteractionEnabled属性在iPad上默认是YES,在iPhone默认是NO,只有设置为YES才可以进行拖动操作
    是CollectionView独有的属性(相对于UITableView),因为其独有的二维网格的布局,因此在重新排序的过程中有时候会发生元素回流了,有时候只是移动到别的位置,不想要这样的效果,就可以修改这个属性改变其相应性

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

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

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

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

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

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

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

  • collectionView:itemsForAddingToDragSession:atIndexPath::该方法是多触摸对应的方法

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

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

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

- (NSArray *)collectionView:(UICollectionView *)collectionView itemsForAddingToDragSession:(id)session atIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point {
    NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];
    UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];
    return @[item];
}
2. UICollectionViewDragDelegate(初始和自定义拖动方法)
  • collectionView:itemsForBeginningDragSession:atIndexPath:提供一个给定indexPath的可进行拖动操作的item(类似hitTest:方法周到该响应的视图)如果返回nil,则不会发生任何拖拽事件
  • 由于是返回一个数组,因此可以根据自己的需求来实现该方法:比如拖拽一个item,就可以把该组的所有项目放进dragSession中,右上角会有小蓝圈圈显示个数(但是这种情况下要对数组进行重新排序,因为数组中的最后一个元素会成为Lift操作中的最上面的一个元素,排序后可以让最先进入dragSession的物品放在lift效果的最前面)
- (NSArray *)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id)session atIndexPath:(NSIndexPath *)indexPath {
    NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];      
    UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];
    self.dragIndexPath = indexPath;
    return @[item];
}
1509605957555404.png
  • collectionView:dragPreviewParametersForItemAtIndexPath:允许对从取消或返回到CollectionView的项目使用自定义预览,如果该方法没有实现或者返回nil,那么整个单元格将用于预览

UIDragPreviewParameters有两个属性

  • backgroundColor设置背景颜色,因为有的视图本身就是半透明的,添加背景色视觉效果更好
  • visiblePath设置视图的可见区域,比如可以自定义为圆角矩形或图中的某个区域等,但是要注意裁剪的Rect在目标视图中必须要有意义;该属性也要标记一下center方便进行定位
- (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生命周期对应的回调方法,可以在这些方法里添加各种动画效果
/ *当电梯动画完成之后开始拖拽之前会调用该方法
 *该方法肯定会对应着-collectionView:dragSessionDidEnd:的调用
 * /
- (void)collectionView:(UICollectionView *)collectionView dragSessionWillBegin:(id)session {
    NSLog(@"dragSessionWillBegin --> drag 会话将要开始");
}
// 拖拽结束的时候会调用该方法
- (void)collectionView:(UICollectionView *)collectionView dragSessionDidEnd:(id)session {
    NSLog(@"dragSessionDidEnd --> drag 会话已经结束");
}
  • 当然也可以在这些方法里面设置自定义的dragPreview,比如iPad中原生的通讯图,地图所展现的功能
3. UICollectionViewDropDelegate(迁移数据和自定义释放动画)
放下手势的流程图
  • collectionView:performDropWithCoordinator:方法使用dropCoordinator去置顶如果处理当前drop会话的item到指定的最终位置,同时也会根据drop项返回的数据更新数据源
  • 当用户开始进行删除操作的时候会调用这个方法
  • 如果该方法不做任何事,将会执行默认的动画
  • 注意:只有在这个方法中才可以请求到数据
  • 请求的方式是异步的,因此不要阻止数据的传输,如果阻止时间过长,就不清楚数据要多久才能到达,系统甚至可能会杀死掉你的应用
- (void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id)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:该方法是提供释放方案的方法,虽然是可选的,但是最好实现
  1. 当跟踪drop行为在tableView空间坐标区域内部时会频繁调用(因此要尽量减少这个方法的工作量,否则帧率就会降低)
  2. 当掉手势在某个部分末端的时候,传递的目标索引路径还不存在(此时indexPath等于该部分的行数),这时候会追加到该部分的末尾
  3. 在某些情况下,目标索引路径可能为空(比如拖到一个没有细胞的空白区域)
  4. 请注意,在某些情况下,你的建议可能不被系统所允许,此时系统将执行不同的建议
  5. 你可以通过 - [session locationInView:]做你自己的命中测试
  6. UICollectionViewDropIntent对应的三个枚举值
  • UICollectionViewDropIntentUnspecified将会接收drop,但是具体的位置要稍后才能确定;不会开启一个缺口,可以通过添加视觉效果给用户传递这一信息
  • UICollectionViewDropIntentInsertAtDestinationIndexPathdrop将会被插入到目标索引中,将会打开一个缺口,模拟最后释放后的布局
  • UICollectionViewDropIntentInsertIntoDestinationIndexPathdrop将会释放在目标索引路径,比如该单元是一个容器(集合),此时不会像?那个属性一样打开缺口,但是该条目标索引对应的单元格会高亮显示
  • 补充:UITableView在以上对应枚举值的基础上,还有一个特有的自动属性,可以自动判断是放入文件夹还是打开缺口进入目标索引
  1. UIDropOperation对应的四种状态。第四种禁止是不允许在当前位置drop:比如要把一个图片放在一个文件夹内,但是这个文件夹是只读的,就会出现这个图标
- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView dropSessionDidUpdate:(id)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:通过该方法判断对应的项目能否被执行drop会话
  1. 如果返回NO,将不会调用接下来的代理方法
  2. 如果没有实现该方法,那么默认返回YES
- (BOOL)collectionView:(UICollectionView *)collectionView canHandleDropSession:(id)session {
    // 假设在该 drop 只能在当前本 app中可执行,在别的 app 中不可以
    if (session.localDragSession == nil) {
        return NO;
    }
    return YES;
}
  • collectionView:dropPreviewParametersForItemAtIndexPath:当item执行drop操作的时候,可以自定义预览图
  1. 如果没有实现该方法或者返回零,整个小区将会被用于预览图
  2. 该方法会经由 - [UICollectionViewDropCoordinator dropItem:toItemAtIndexPath:]调用
  3. 如果要去自定义占位的话,可以查看UICollectionViewDropPlaceholder.previewParametersProvider
- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dropPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath {
    return nil;
}
  • 当然还有一些常规的drop过程回调的方法
/* 当drop会话进入到 collectionView 的坐标区域内就会调用,
 * 早于- [collectionView dragSessionWillBegin] 调用
 */
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnter:(id)session {
    NSLog(@"dropSessionDidEnter --> dropSession进入目标区域");
}
/* 当 dropSession 不在collectionView 目标区域的时候会被调用
 */
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidExit:(id)session {
    NSLog(@"dropSessionDidExit --> dropSession 离开目标区域");
}
/* 当dropSession 完成时会被调用,不管结果如何
 * 适合在这个方法里做一些清理的操作
 */
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnd:(id)session {
    NSLog(@"dropSessionDidEnd --> dropSession 已完成");
}
4.优化
  • 涉及到app间拖动的时候,比如把相册中照片拖到邮件中,为什么相册中的小尺寸到了邮件中就刚刚和邮件中textView宽度一致呢?
  • 在方法collectionView:itemsForBeginningDragSession:atIndexPath:中,通过设置itemProvider.preferredPresentationSize来设置项执行drop时的期望大小。这样的邮件应用程序在后台就能读取到这个尺寸大小,从而正常地显示
5.占位符
由于加载对象是异步的,因此加载数据和显示预览是两条不同的时间线
  • 使用场景:拖拽的项目需要从服务器下载,比如拖拽相册中存储在iCloud中的照片至邮件应用中,就要先从iCloud下载,再进行下一步的展示,因此可能要等待一段时间才能下载完成,而且下载多个项目还可能是乱序到达的。此时就需要占位符进行
  • 异步加载数据的时候可以用PlaceHolder推迟更新数据源直到数据加载完毕,从而保证UI完全的响应性,不至于让用户长时间面对一个白板等待数据的传输
  • 如何创建占位符?通过释放协调器dropCoordinator来创建,从而将其插入到占位符中,并添加动画
    使用PlaceHolder注意事项:(app间拖拽的时候,从A应用程序拖拖到B应用程序,确定位置之后,B中还未获取到数据,加载数据的过程中展示占位动画)
- (id)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返回一个进度
  • 调用一次loadObjectOfClass只会返回一个特定的进度,通过KVO监听UIDropSession.progress可以获得所有的进度
  • demo是针对iPhone开发的,因此没有具体实现
// 该方法中加载数据的方式是异步的,
NSProgress *progress = [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^(id  _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纯文本”代替“public.plain文本” ,当然如果是仅支持公司内部特定的应用程序间传递,​​也可以完全自定义
  • 新概念:数据编组(Data Marshaling)
    提供数据有三种方式:
  1. 直接提供的NSData:itemProvider.registerDataRepresentation(...)
  2. 提供一个文件或者文件夹:itemProvider.registerFileRepresentation(... fileOptions:[])
  3. 作为File Provider的引用:itemProvider.registerFileRepresentation(... fileOptions:[。openInPlace])
    接收数据也有三种方式:
  4. 直接拷贝出NSData的副本:itemProvider.loadDataRepresentation(...)
  5. 将文件或文件夹拷贝到自己的容器内:itemProvider.loadDataRepresentation(...)
  6. 尝试在本地打开文件:itemProvider.loadInPlaceFileRepresentation(...)
    数据编组直接做好了数据的转换
  7. 提供者想要提供一个NSData类型数据,数据编组就就直接将这个数据写入文件并提供url的副本
  8. 如果提供者提供的是文件夹,然后数据编组就会把文件压缩并提供NSData的
  • 最后稍微提到了文件的另一个主题,也就是文件系统的拖拽,在这里大概叙述一下
    文件的拖拽可以设置三种权限
  1. 对所有人可见
  2. 同一个队可见
  3. 仅对自己可见
    文件的拖拽有两种选项:
  4. 直接提供副本
  5. 提供URL(意味着多个应用程序可以共享一个文件),对方修改,本地可以看到修改的地方

三. UIView的吸头

  • UITableView的api使用基本和UICollectionView一致,在此不再赘述,但是以下UIView的特性还要再强调以下
  1. iPhone项目上,在对视图添加UIDragInteraction操作时,一定要设置其enable属性为YES,否则不会响应拖动操作(iPhone默认为NO,iPad默认为YES)
  2. UIDropProposal的属性精确,如果设置为YES,视图的点击测试区域将略高于用户触摸位置,这能够在视图中进行更精确的放入,具体效果请看下图
  3. 当然如果使用这个属性的话要在targetPoint添加一些UI的提示,给用户确切的反馈


    这样就能精准地放入文本中的特定位置
  4. prefersFullSizePreview,默认情况下预览图都是等比例缩小的,因为过大是没有意义的,遮挡屏幕就会影响到用户交互,难以进行导航,但是有些时候也需要全尺寸的预览图(比如一个列表中需要重新布局,此时将整个列表缩小是没有意义的)
  • 但是有些情况下,系统始终会进行比例缩小,即使是设置了全尺寸预览
    组合拖动:如果添加多个项目进行拖动
    如果将项目拖动到另外一个应用程序,也肯定会等比例缩小
  • [itemProvider loadObjectOfClass:completionHandler:]
  1. 该方法回调默认在主线程
  2. 该方法返回一个进步,汇报加载的进度
  3. 返回值NSProgress可以设置属性取消和^ cancellationHandler,也可以进行断点续传操作,因为数据传输可能需要很久,需要给用户取消的权利
  4. 如果不想要显示这个进度,可以通过session.progressIndicatorStyle = UIDropSessionProgressIndicatorStyleNone; 来隐藏进度视图。
  5. 也可以通过志愿监听进度实现自定义进度展示
    摘录自:史上第二走心的iOS11-Drog & Drag

推荐,关注

1570756780.png

你可能感兴趣的:(iOS11-DragAndDrop拖放教程)