文章分享至个人我的技术博客: https://cainluo.github.io/15102983446918.htmlhtml
还记得在WWDC 2017
的时候, 苹果爸爸展现的拖放功能是多么的牛逼, 实现了可夸应用的数据分享.ios
若是你有看过以前的玩转iOS开发:iOS 11 新特性《UIKit新特性的基本认识》, 那么你应该有一个基础的认识, 若是没有也不要紧, 由于你正在看着这篇文章.git
这里咱们会用一个针对iPad Pro 10.5
英寸的小项目进行演示.github
转载声明:如须要转载该文章, 请联系做者, 而且注明出处, 以及不能擅自修改本文.json
这里我打算使用Storyboard
来做为主开发的工具, 为了省下过多的布局代码.微信
这是模仿一个顾客去买水果的场景, 这里面的布局也不算难, 主要逻辑:session
ListController
分别管理.ListController
主要是显示一个UICollectionView
, 而咱们拖放也是在ListController
里实现的.简单的写了一下数据模型, 而且控制一下对应的数据源, 咱们就能够看到简单的界面了:ide
配置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);
复制代码
-dropInteraction:performDrop:
这个方法不被调用.-dropInteraction:performDrop:
这个方法里进行处理.第二个部分是UICollectionViewDropIntent
:
typedef NS_ENUM(NSInteger, UICollectionViewDropIntent) {
UICollectionViewDropIntentUnspecified,
UICollectionViewDropIntentInsertAtDestinationIndexPath,
UICollectionViewDropIntentInsertIntoDestinationIndexPath,
} API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos, watchos);
复制代码
看到这里, 若是咱们要以明确的方式显示给用户的话, 咱们就要选中其中一种组合, 什么组合? 看代码呗:
- (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
, 具体是取决于这个拖动操做是移动仍是从新排序, 因此咱们这里要有一个能够咨询管理者是什么类型的拖动.需求咱们有了, 如今就来实现代码了, 先创建一个索引管理者:
- (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;
复制代码
- (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
:
- (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;
复制代码
- (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