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

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

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


话不多说,先上效果图


普通view拖拽效果


TableView拖拽效果


CollectionView效果


muti-touch效果


多app交互


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


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


1. Drag 和 Drop 是什么呢?


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

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

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


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层级的改变



4. Others


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


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];

}


再放一遍这个效果图


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];

}



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


  • UIDragPreviewParameters 有两个属性:


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

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



裁剪图中的某一块区域



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


再高级的功能可以实现目标区域内添加多个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 中原生的通讯图、地图所展现的功能



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


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


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:比如要把一个图片放在一个文件夹内,但是这个文件夹是只读的,就会出现这个图标



- (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 间传递数据,我想史上第一走心的教程一定会详细讲述这个方法的


由于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的提示,给用户确切的反馈


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


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


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


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

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


  • [itemProvider loadObjectOfClass: completionHandler:]


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

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

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

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

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


方法控制效果


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

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




  • 作者:si1ence

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

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

【点击成为安卓大神】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值