玩转iOS开发:iOS 11 新特性《UICollectionView的拖放》

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


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

若是你有看过以前的玩转iOS开发:iOS 11 新特性《UIKit新特性的基本认识》, 那么你应该有一个基础的认识, 若是没有也不要紧, 由于你正在看着这篇文章.git

这里咱们会用一个针对iPad Pro 10.5英寸的小项目进行演示.github

转载声明:如须要转载该文章, 请联系做者, 而且注明出处, 以及不能擅自修改本文.json

工程的配置

这里我打算使用Storyboard来做为主开发的工具, 为了省下过多的布局代码.微信

1

这是模仿一个顾客去买水果的场景, 这里面的布局也不算难, 主要逻辑:session

  • 主容器控制器嵌入两个比较小的视图控制器, 经过ListController分别管理.
  • ListController主要是显示一个UICollectionView, 而咱们拖放也是在ListController里实现的.

简单的写了一下数据模型, 而且控制一下对应的数据源, 咱们就能够看到简单的界面了:ide

2

配置拖放的功能

配置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时会有长按拖放效果了:

3

配置拖放的"放"效果

拖放效果有了, 但问题来了, 当咱们拖放到另外一个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的右上角会有一个绿色的加好:

4

配置你的意图

咱们在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的选项也更加符合咱们的需求.

模型数据的协调者

虽然苹果爸爸给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<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;
}
复制代码

最后面咱们来实现这个UICollectionViewUICollectionViewDragDelegate, 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, 写的时候没有考虑到

最终的效果:

5

6

工程

https://github.com/CainRun/iOS-11-Characteristic/tree/master/4.DragDrop


最后

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

微信

支付宝
相关文章
相关标签/搜索