文章分享至我的个人技术博客: https://cainluo.github.io/15102983446918.html
还记得在WWDC 2017
的时候, 苹果爸爸展示的拖放功能是多么的牛逼, 实现了可夸应用的数据分享.
如果你有看过之前的玩转iOS开发:iOS 11 新特性《UIKit新特性的基本认识》, 那么你应该有一个基础的认识, 如果没有也没关系, 因为你正在看着这篇文章.
这里我们会用一个针对iPad Pro 10.5
英寸的小项目进行演示.
转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.
工程的配置
这里我打算使用Storyboard
来作为主开发的工具, 为了省下过多的布局代码.
这是模仿一个顾客去买水果的场景, 这里面的布局也不算难, 主要逻辑:
- 主容器控制器嵌入两个比较小的视图控制器, 通过
ListController
分别管理. ListController
主要是显示一个UICollectionView
, 而我们拖放也是在ListController
里实现的.
简单的写了一下数据模型, 并且控制一下对应的数据源, 我们就可以看到简单的界面了:
配置拖放的功能
配置UICollectionView
其实是非常容易的, 我们只需要将一个声明UICollectionViewDragDelegate
代理的实例赋值给UICollectionView
, 然后再实现一个方法就可以了.
接下来这里, 我们设置一下拖放的代理, 并且实现必要的拖放代理方法:
self.collectionView.dragDelegate = self;
self.collectionView.dropDelegate = self;
#pragma mark - Collection View Drag Delegate
- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView
itemsForBeginningDragSession:(id<UIDragSession>)session
atIndexPath:(NSIndexPath *)indexPath {
NSItemProvider *itemProvider = [[NSItemProvider alloc] init];
UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];
return @[item];
}
这样子我们就可以看到长按CollectionView
时会有长按拖放效果了:
配置拖放的"放"效果
拖放效果有了, 但问题来了, 当我们拖放到另一个UICollectionView
松手时, 会发现并不能将数据拖放过去, 其实是我们并没有配置UICollectionViewDropDelegate
代理, 这个和刚刚的配置方法一样, 这里就不多说了.
首先我们来实现一个方法:
- (BOOL)collectionView:(UICollectionView *)collectionView
canHandleDropSession:(id<UIDropSession>)session {
return session.localDragSession != nil ? YES : NO;
}
这个可选方法是在咨询你会否愿意处理拖放, 我们可以通过实现这个方法来限制从同一个应用发起的拖放会话.
这个限制是通过UIDropSession
中的localDragSession
进行限制, 如果为YES
, 则表示接受拖放, 如果为NO
, 就表示不接受.
讲完这个之后, 我们来看看UICollectionViewDropDelegate
唯一一个要实现的方法, 这个方法要有相应, 是根据上面的那个方法是返回YES
还是返回NO
来判断的:
- (void)collectionView:(UICollectionView *)collectionView
performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
}
然后我们配置好UICollectionViewDropDelegate
的代理对象, 再试试拖放效果, 机会发现拖到隔壁的UICollectionView
的右上角会有一个绿色的加好:
配置你的意图
我们在UICollectionView
里拖动一个对象的时候, UICollectionView
会咨询我们的意图, 然后根据我们不同的配置, 就会做出不同的反应.
这里我们要分成两个部分, 第一个部分是一个叫做UIDropOperation
:
typedef NS_ENUM(NSUInteger, UIDropOperation) {
UIDropOperationCancel = 0,
UIDropOperationForbidden = 1,
UIDropOperationCopy = 2,
UIDropOperationMove = 3,
} API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos);
- UIDropOperationCancel: 表示取消拖动操作, 如果是使用这个枚举的话, 会导致
-dropInteraction:performDrop:
这个方法不被调用. - UIDropOperationForbidden: 表示该操作被禁止, 如果你是使用这个枚举的话, 在拖放时会显示一个🚫的图标, 表示该操作被禁止.
- UIDropOperationCopy: 表示从数据源里赋值对应的数据, 会在
-dropInteraction:performDrop:
这个方法里进行处理. - UIDropOperationMove: 表示移动数据源里对应的数据, 将对应的数据从数据源里移动到目标的地方.
第二个部分是UICollectionViewDropIntent
:
typedef NS_ENUM(NSInteger, UICollectionViewDropIntent) {
UICollectionViewDropIntentUnspecified,
UICollectionViewDropIntentInsertAtDestinationIndexPath,
UICollectionViewDropIntentInsertIntoDestinationIndexPath,
} API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos, watchos);
- UICollectionViewDropIntentUnspecified: 表示操作即将被拖放的视图, 但这个位置并不会以明确的方式显示出来
- UICollectionViewDropIntentInsertAtDestinationIndexPath: 表示被拖放的视图会模拟最终放置效果, 也就是说会在目标位置离打开一个空白的地方来模拟最终插入的目标位置.
- UICollectionViewDropIntentInsertIntoDestinationIndexPath: 表示将拖放的视图放置对应的索引中, 但这个位置并不会以明确的方式显示出来
看到这里, 如果我们要以明确的方式显示给用户的话, 我们就要选中其中一种组合, 什么组合? 看代码呗:
- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView
dropSessionDidUpdate:(id<UIDropSession>)session
withDestinationIndexPath:(NSIndexPath *)destinationIndexPath {
return [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove
intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
}
这种组合可以在我们拖放移动视图的时候有一个显式的动画, 并且UIDropOperationMove
的选项也更加符合我们的需求.
模型数据的协调者
虽然苹果爸爸给UICollectionView
和UITableView
添加的拖放效果很好, 但有一样东西是做的并不是很好, 这个就是处理我们的模型层, 这个需要我们开发者自己的捣鼓, 我猜在未来苹果爸爸会在这一块里下手, 从而减少我们的开发者的工作, 当然这只是猜测.
根据我们的拖放交互的复杂性, 我们有两种方案可以采取:
- 如果在不同类的两个视图之间拖动单个数据, 比如自定义的
UIView
和UICollectionView
, 我们可以通过localObject
这个属性将模型对象附加到UIDragItem
中, 当我们收到拖放时, 我们就可以从拖放管理者里通过localObject
里检索模型对象. - 如果是在两个或者是基于多个的集合类视图拖放一个或者是多个数据(比如
UITableView
和UITableView
,UICollectionView
和UICollectionView
,UITableView
和UICollectionView
), 并且需要跟踪哪些索引路径会受到影响以及哪些数据被拖动, 那么在第一个中方案里是做不到的, 相反, 如果我们创建一个可以跟踪事物的自定义拖放管理者, 那么我们就可以实现了, 比如在源视图, 目标视图里拖动单个或者是多个数据, 然后在这个自定义管理者中传递这个在拖放操作中使用UIDragSession
中的localContext
属性.
我们这里使用的就是第二种方式.
创建模型数据协调者
既然刚刚我们说了要捣鼓一个管理者, 那我们先想一想这个管理者要做哪一些工作, 才能够完成这个拖放并且实现模型更新的操作:
- 拖动的时候可以找到对应的数据源, 可以进行删除操作.
- 存储被拖动数据源的索引路径.
- 目标数据源, 当我们拖放数据源到指定位置的时候可以知道是在哪里.
- 找到拖放数据源将要插入的索引路径.
- 拖放项目将被插入的索引路径
- 这里有一个场景要说明, 如果我们只是移动或者是重新排序的话, 我们要利用
UICollectionView
提供的API
, 具体是取决于这个拖动操作是移动还是重新排序, 所以我们这里要有一个可以咨询管理者是什么类型的拖动. - 当所有步骤都完成了, 我们就可以更新源集合视图了.
需求我们有了, 现在就来实现代码了, 先建立一个索引管理者:
ListModelCoordinator.h
- (instancetype)initWithSource:(ListModelType)source;
- (UIDragItem *)dragItemForIndexPath:(NSIndexPath *)indexPath;
- (void)calculateDestinationIndexPaths:(NSIndexPath *)indexPath
count:(NSInteger)count;
@property (nonatomic, assign, getter=isReordering) BOOL reordering;
@property (nonatomic, assign) BOOL dragCompleted;
@property (nonatomic, strong) NSMutableArray *sourceIndexes;
@property (nonatomic, strong) NSMutableArray<NSIndexPath *> *sourceIndexPaths;
@property (nonatomic, strong) NSArray<NSIndexPath *> *destinationIndexPaths;
@property (nonatomic, strong) ListDataModel *listModel;
@property (nonatomic, assign) ListModelType source;
@property (nonatomic, assign) ListModelType destination;
ListModelCoordinator.m
- (BOOL)isReordering {
return self.source == self.destination;
}
- (instancetype)initWithSource:(ListModelType)source {
self = [super init];
if (self) {
self.source = source;
}
return self;
}
- (NSMutableArray<NSIndexPath *> *)sourceIndexPaths {
if (!_sourceIndexPaths) {
_sourceIndexPaths = [NSMutableArray array];
}
return _sourceIndexPaths;
}
- (NSMutableArray *)sourceIndexes {
if (!_sourceIndexes) {
_sourceIndexes = [NSMutableArray array];
[_sourceIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[_sourceIndexes addObject:@(obj.item)];
}];
}
return _sourceIndexes;
}
- (UIDragItem *)dragItemForIndexPath:(NSIndexPath *)indexPath {
[self.sourceIndexPaths addObject:indexPath];
return [[UIDragItem alloc] initWithItemProvider:[[NSItemProvider alloc] init]];
}
- (void)calculateDestinationIndexPaths:(NSIndexPath *)indexPath
count:(NSInteger)count {
NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForItem:indexPath.item
inSection:0];
NSMutableArray *indexPathArray = [NSMutableArray arrayWithObject:destinationIndexPath];
self.destinationIndexPaths = [indexPathArray copy];
}
创建完这个索引管理者之后, 我们还要有一个根据这个索引管理者去管理数据源的ViewModel
:
FruitStandViewModel.h
- (instancetype)initFruitStandViewModelWithController:(UIViewController *)controller;
@property (nonatomic, strong, readonly) NSMutableArray *dataSource;
- (ListDataModel *)getDataModelWithIndexPath:(NSIndexPath *)indexPath
context:(ListModelType)context;
- (NSMutableArray *)deleteModelWithIndexes:(NSArray *)indexes
context:(ListModelType)context;
- (void)insertModelWithDataSource:(NSArray *)dataSource
context:(ListModelType)contexts
index:(NSInteger)index;
FruitStandViewModel.m
- (instancetype)initFruitStandViewModelWithController:(UIViewController *)controller {
self = [super init];
if (self) {
self.fruitStandController = (FruitStandController *)controller;
}
return self;
}
- (ListDataModel *)getDataModelWithIndexPath:(NSIndexPath *)indexPath
context:(ListModelType)context {
NSArray *dataSource = self.dataSource[context];
ListDataModel *model = dataSource[indexPath.row];
return model;
}
- (NSMutableArray *)deleteModelWithIndexes:(NSArray *)indexes
context:(ListModelType)context {
NSMutableArray *array = [NSMutableArray array];
[indexes enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSInteger idex = [obj integerValue];
ListDataModel *dataModel = self.dataSource[context][idex];
[array addObject:dataModel];
}];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self.dataSource[context] removeObject:obj];
}];
return array;
}
- (void)insertModelWithDataSource:(NSArray *)dataSource
context:(ListModelType)context
index:(NSInteger)index {
[self.dataSource[context] insertObjects:dataSource
atIndexes:[NSIndexSet indexSetWithIndex:index]];
}
- (NSMutableArray *)dataSource {
if (!_dataSource) {
_dataSource = [NSMutableArray array];
NSData *JSONData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"data"
ofType:@"json"]];
NSDictionary *jsonArray = [NSJSONSerialization JSONObjectWithData:JSONData
options:NSJSONReadingMutableLeaves
error:nil];
NSArray *data = jsonArray[@"data"];
for (NSArray *dataArray in data) {
[_dataSource addObject:[NSArray yy_modelArrayWithClass:[ListDataModel class]
json:dataArray]];
}
}
return _dataSource;
}
最后面我们来实现这个UICollectionView
的UICollectionViewDragDelegate
, UICollectionViewDropDelegate
代理方法:
#pragma mark - Collection View Drag Delegate
- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView
itemsForBeginningDragSession:(id<UIDragSession>)session
atIndexPath:(NSIndexPath *)indexPath {
ListModelCoordinator *listModelCoordinator = [[ListModelCoordinator alloc] initWithSource:self.context];
ListDataModel *dataModel = self.fruitStandViewModel.dataSource[self.context][indexPath.row];
listModelCoordinator.listModel = dataModel;
session.localContext = listModelCoordinator;
return @[[listModelCoordinator dragItemForIndexPath:indexPath]];
}
- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView
itemsForAddingToDragSession:(id<UIDragSession>)session
atIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point {
if ([session.localContext class] == [ListModelCoordinator class]) {
ListModelCoordinator *listModelCoordinator = (ListModelCoordinator *)session.localContext;
return @[[listModelCoordinator dragItemForIndexPath:indexPath]];
}
return nil;
}
- (void)collectionView:(UICollectionView *)collectionView
dragSessionDidEnd:(id<UIDragSession>)session {
if ([session.localContext class] == [ListModelCoordinator class]) {
ListModelCoordinator *listModelCoordinator = (ListModelCoordinator *)session.localContext;
listModelCoordinator.source = self.context;
listModelCoordinator.dragCompleted = YES;
if (!listModelCoordinator.isReordering) {
[collectionView performBatchUpdates:^{
[collectionView deleteItemsAtIndexPaths:listModelCoordinator.sourceIndexPaths];
} completion:^(BOOL finished) {
}];
}
}
}
#pragma mark - Collection View Drop Delegate
- (BOOL)collectionView:(UICollectionView *)collectionView
canHandleDropSession:(id<UIDropSession>)session {
return session.localDragSession != nil ? YES : NO;
}
- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView
dropSessionDidUpdate:(id<UIDropSession>)session
withDestinationIndexPath:(NSIndexPath *)destinationIndexPath {
return [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove
intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
}
- (void)collectionView:(UICollectionView *)collectionView
performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
if (!coordinator.session.localDragSession.localContext) {
return;
}
ListModelCoordinator *listModelCoordinator = (ListModelCoordinator *)coordinator.session.localDragSession.localContext;
NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForItem:[collectionView numberOfItemsInSection:0]
inSection:0];
NSIndexPath *indexPath = coordinator.destinationIndexPath ? : destinationIndexPath;
[listModelCoordinator calculateDestinationIndexPaths:indexPath
count:coordinator.items.count];
listModelCoordinator.destination = self.context;
[self moveItemWithCoordinator:listModelCoordinator
performingDropWithDropCoordinator:coordinator];
}
#pragma mark - Private Method
- (void)moveItemWithCoordinator:(ListModelCoordinator *)listModelCoordinator
performingDropWithDropCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
NSArray *destinationIndexPaths = listModelCoordinator.destinationIndexPaths;
if (listModelCoordinator.destination != self.context || !destinationIndexPaths) {
return;
}
NSMutableArray *dataSourceArray = [self.fruitStandViewModel deleteModelWithIndexes:listModelCoordinator.sourceIndexes
context:listModelCoordinator.source];
[coordinator.items enumerateObjectsUsingBlock:^(id<UICollectionViewDropItem> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSIndexPath *sourceIndexPath = listModelCoordinator.sourceIndexPaths[idx];
NSIndexPath *destinationIndexPath = destinationIndexPaths[idx];
[self.collectionView performBatchUpdates:^{
[self.fruitStandViewModel insertModelWithDataSource:@[dataSourceArray[idx]]
context:listModelCoordinator.destination
index:destinationIndexPath.item];
if (listModelCoordinator.isReordering) {
[self.collectionView moveItemAtIndexPath:sourceIndexPath
toIndexPath:destinationIndexPath];
} else {
[self.collectionView insertItemsAtIndexPaths:@[destinationIndexPath]];
}
} completion:^(BOOL finished) {
}];
[coordinator dropItem:obj.dragItem
toItemAtIndexPath:destinationIndexPath];
}];
listModelCoordinator.dragCompleted = YES;
}
这里面的用法和之前UITableView
的用法有些类似, 但由于是跨视图的原因会有一些差异.
而且这里只是作为一个演示的Demo, 写的时候没有考虑到
最终的效果:
工程
https://github.com/CainRun/iOS-11-Characteristic/tree/master/4.DragDrop