博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
玩转iOS开发:iOS 11 新特性《UICollectionView的拖放》
阅读量:6993 次
发布时间:2019-06-27

本文共 14742 字,大约阅读时间需要 49 分钟。

文章分享至我的个人技术博客: https://cainluo.github.io/15102983446918.html


还记得在WWDC 2017的时候, 苹果爸爸展示的拖放功能是多么的牛逼, 实现了可夸应用的数据分享.

如果你有看过之前的, 那么你应该有一个基础的认识, 如果没有也没关系, 因为你正在看着这篇文章.

这里我们会用一个针对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
*)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id
)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
)session { return session.localDragSession != nil ? YES : NO;}复制代码

这个可选方法是在咨询你会否愿意处理拖放, 我们可以通过实现这个方法来限制从同一个应用发起的拖放会话.

这个限制是通过UIDropSession中的localDragSession进行限制, 如果为YES, 则表示接受拖放, 如果为NO, 就表示不接受.

讲完这个之后, 我们来看看UICollectionViewDropDelegate唯一一个要实现的方法, 这个方法要有相应, 是根据上面的那个方法是返回YES还是返回NO来判断的:

- (void)collectionView:(UICollectionView *)collectionViewperformDropWithCoordinator:(id
)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
)session withDestinationIndexPath:(NSIndexPath *)destinationIndexPath { return [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];}复制代码

这种组合可以在我们拖放移动视图的时候有一个显式的动画, 并且UIDropOperationMove的选项也更加符合我们的需求.

模型数据的协调者

虽然苹果爸爸给UICollectionViewUITableView添加的拖放效果很好, 但有一样东西是做的并不是很好, 这个就是处理我们的模型层, 这个需要我们开发者自己的捣鼓, 我猜在未来苹果爸爸会在这一块里下手, 从而减少我们的开发者的工作, 当然这只是猜测.

根据我们的拖放交互的复杂性, 我们有两种方案可以采取:

  1. 如果在不同类的两个视图之间拖动单个数据, 比如自定义的UIViewUICollectionView, 我们可以通过localObject这个属性将模型对象附加到UIDragItem中, 当我们收到拖放时, 我们就可以从拖放管理者里通过localObject里检索模型对象.
  2. 如果是在两个或者是基于多个的集合类视图拖放一个或者是多个数据(比如UITableViewUITableView, UICollectionViewUICollectionView, UITableViewUICollectionView), 并且需要跟踪哪些索引路径会受到影响以及哪些数据被拖动, 那么在第一个中方案里是做不到的, 相反, 如果我们创建一个可以跟踪事物的自定义拖放管理者, 那么我们就可以实现了, 比如在源视图, 目标视图里拖动单个或者是多个数据, 然后在这个自定义管理者中传递这个在拖放操作中使用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
*sourceIndexPaths;@property (nonatomic, strong) NSArray
*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
*)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;}复制代码

最后面我们来实现这个UICollectionViewUICollectionViewDragDelegate, UICollectionViewDropDelegate代理方法:

#pragma mark - Collection View Drag Delegate- (NSArray
*)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id
)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
*)collectionView:(UICollectionView *)collectionView itemsForAddingToDragSession:(id
)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
)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
)session { return session.localDragSession != nil ? YES : NO;}- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView dropSessionDidUpdate:(id
)session withDestinationIndexPath:(NSIndexPath *)destinationIndexPath { return [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];}- (void)collectionView:(UICollectionView *)collectionViewperformDropWithCoordinator:(id
)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:listModelCoordinatorperformingDropWithDropCoordinator:coordinator];}#pragma mark - Private Method- (void)moveItemWithCoordinator:(ListModelCoordinator *)listModelCoordinatorperformingDropWithDropCoordinator:(id
)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
_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


最后

码字很费脑, 看官赏点饭钱可好

你可能感兴趣的文章
webpack 打包非utf-8编码包的解决办法!
查看>>
探秘Spring AOP (四) Spring AOP 使用讲解 3
查看>>
springboot logback日志配置
查看>>
(一)MR编写之读取linux上(指定目录下的)配置文件
查看>>
springboot+jersay环境下解决long类型到前台精度丢失的问题
查看>>
K-Means聚类的一些小问题
查看>>
Java项目导出源代码jar包在Eclipse中查看中文注释乱码的问题
查看>>
SpringSecurity 学习总结
查看>>
Javascript之JS盒子模型常用的属性、定时器、获取浏览器计算后的属性样式值
查看>>
改变世界的七大NLP技术,你了解多少?(下)
查看>>
通过机器学习和人工智能实现SDN
查看>>
阿里云API网关常见应用场景
查看>>
css和javascript在IE和Firefox中二十三个不同点
查看>>
android精品源码,下拉刷新效果高德地图五子棋游戏定制日历全民TV源码
查看>>
我的友情链接
查看>>
BTREE,mysql索引
查看>>
MySQL数据库开发必备常识
查看>>
不停止MySQL服务增加从库的两种方式
查看>>
Oracle 10g升级之--PSU 升级(续)
查看>>
通过案例学调优之--Oracle 全文索引
查看>>