话不多说,先上效果图
普通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 脱离屏幕
DragSet DownData Transfer
- 这些都是围绕交互这一概念构造的:即类似手势识别器的概念,接收到用户的操作后,进行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注意点总结:
dragInteractionEnabledreorderingCadenceUICollectionViewReorderingCadenceImmediate
- :默认值,当开始移动的时候就立即回流集合视图布局,可以理解为实时的重新排序
UICollectionViewReorderingCadenceFast
- :如果你快速移动,CollectionView 不会立即重新布局,只有在停止移动的时候才会重新布局
UICollectionViewReorderingCadenceSlow
- :停止移动再过一会儿才会开始回流,重新布局
springLoaded
- UITableView 和 UICollectionView 都可以使用该方式加载,因为他们都遵守 UISpringLoadedInteractionSupporting 协议
- 当用户在单元格使用弹性加载时,我们要选择 CollectionView 或tableView 中的 item 或cell
- 使用
- (BOOL)collectionView:shouldSpringLoadItemAtIndexPath:withContext:
- 来自定义也是可以的
collectionView:itemsForAddingToDragSession: atIndexPath:
- 当接收到添加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 有两个属性:
backgroundColorvisiblePath
- 设置视图的可见区域,比如可以自定义为圆角矩形或图中的某一块区域等,但是要注意裁剪的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
• 对应的三个枚举值
UICollectionViewDropIntentUnspecifiedUICollectionViewDropIntentInsertAtDestinationIndexPathdrop
• 将会被插入到目标索引中;将会打开一个缺口,模拟最后释放后的布局
UICollectionViewDropIntentInsertIntoDestinationIndexPathdrop