玩轉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


最後

碼字很費腦, 看官賞點飯錢可好

微信

支付寶
相關文章
相關標籤/搜索