Good articles to my personal technology blog: https://cainluo.github.io/15102983446918.html


Remember at WWDC 2017, apple dad showed off how awesome drag-and-drop was, enabling data sharing in apps that can be praised.

If you’ve read iOS Development: The Basics of UIKit’s new features in iOS 11, you should have a basic idea. If you don’t, that’s ok, because you’re reading this.

We’ll demonstrate this with a small project for the iPad Pro 10.5 inches.

Reprint statement: if you need to reprint this article, please contact the author, and indicate the source, and can not modify this article without authorization.

Project configuration

I’m going to use Storyboard as the main developer tool here to save too much layout code.

This is an imitation of a customer to buy fruit scene, the layout is not difficult, the main logic:

  • The main container controller embeds two smaller view controllers throughListControllerManage separately.
  • ListControllerIt’s basically showing oneUICollectionViewAnd we drag and dropListControllerIn the realization of.

After simply writing the data model and controlling the corresponding data source, we can see a simple interface:

Configure drag and drop

Configuration UICollectionView is actually very easy, we just need to a proxy statement UICollectionViewDragDelegate instance assigned to UICollectionView, then a method can achieve them.

Next, let’s set up the drag-and-drop proxy and implement the necessary drag-and-drop proxy methods:

    self.collectionView.dragDelegate = self;
    self.collectionView.dropDelegate = self;
Copy the code
#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];
}
Copy the code

Now we can see the drag-and-drop effect when we hold down the CollectionView:

Configure the drag and drop “drop” effect

Drag effect had, but the problem is coming, when we drag and drop another UICollectionView let go, will find that and not be able to drag and drop the past data, is we don’t have any configuration UICollectionViewDropDelegate agent, just this and configuration method, I won’t go into that here.

First let’s implement a method:

- (BOOL)collectionView:(UICollectionView *)collectionView
  canHandleDropSession:(id<UIDropSession>)session {
  
    returnsession.localDragSession ! =nil ? YES : NO;
}
Copy the code

This optional method is to ask if you would like to handle drag-and-drop, and we can implement this method to limit drag-and-drop sessions initiated from the same application.

This restriction is limited by localDragSession in UIDropSession, which means drag and drop is accepted if it is YES, and not if it is NO.

After finish this, let’s take a look at UICollectionViewDropDelegate only one method, this method should have corresponding, is based on the above method is to return YES or to return NO:

- (void)collectionView:(UICollectionView *)collectionView
performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
    
}
Copy the code

Then we configured UICollectionViewDropDelegate proxy objects, and then try dragging effect, opportunity discovery will drag the top right-hand corner of the UICollectionView next door has a green bonus:

Configure your intentions

When we drag an object in the UICollectionView, the UICollectionView will consult with us about our intentions and then react differently depending on our configuration.

Here we are going to split it into two parts. The first part is called UIDropOperation:

typedef NS_ENUM(NSUInteger.UIDropOperation) {
    UIDropOperationCancel    = 0.UIDropOperationForbidden = 1.UIDropOperationCopy      = 2.UIDropOperationMove      = 3,
} API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos);
Copy the code
  • UIDropOperationCancel:Cancels the drag operation, which would result if the enumeration was used-dropInteraction:performDrop:This method is not called.
  • UIDropOperationForbidden: said the operation is prohibited, if you are using this enumeration, a 🚫 icon is displayed when drag and drop, said the operation is prohibited.
  • UIDropOperationCopy:Indicates that the corresponding data assigned from the data source will be in-dropInteraction:performDrop:I’m going to do it in this method.
  • UIDropOperationMove: Represents to move the corresponding data in the data source to the target from the data source.

The second part is the UICollectionViewDropIntent:

typedef NS_ENUM(NSInteger.UICollectionViewDropIntent) {

    UICollectionViewDropIntentUnspecified.UICollectionViewDropIntentInsertAtDestinationIndexPath.UICollectionViewDropIntentInsertIntoDestinationIndexPath,
} API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos, watchos);
Copy the code
  • UICollectionViewDropIntentUnspecified: said to drag and drop operation view, but the position will not be displayed in a clear way
  • UICollectionViewDropIntentInsertAtDestinationIndexPath: said by drag and drop the view of simulation eventually placed effect, that is to say, the target position from opening a blank place to simulate the end inserted into the target location.
  • UICollectionViewDropIntentInsertIntoDestinationIndexPath: place the drag and drop the view of the corresponding index, but the position will not be displayed in a clear way

So here, if we want to show it to the user in an explicit way, we have to select one of these combinations. What combination? Look at the code:

- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView
                            dropSessionDidUpdate:(id<UIDropSession>)session
                        withDestinationIndexPath:(NSIndexPath *)destinationIndexPath {

    return [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove
                                                                intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
}
Copy the code

This combination allows us to have an explicit animation when we drag and drop the move view, and the UIDropOperationMove option fits our needs better.

Coordinator of model data

While apple’s drag-and-drop additions to UICollectionView and UITableView are great, there’s one thing they don’t do very well, and that’s our model layer, which we have to work on ourselves, and I suspect apple’s going to do that in the future. That would reduce our developers’ work, but that’s just speculation.

Depending on the complexity of our drag-and-drop interaction, we have two options:

  1. If you drag a single piece of data between views of different classes, such as customUIViewandUICollectionViewWe can go throughlocalObjectThis property appends the model object toUIDragItemWhen we receive a drag and drop, we can pass it through the drag and drop managerlocalObjectRetrieves model objects in.
  2. Drag and drop one or more data from two or more collection class views (e.gUITableViewandUITableView.UICollectionViewandUICollectionView.UITableViewandUICollectionView), and the need to track what index path will be affected and what data is being dragged, so in the first scheme is made, on the contrary, if we create a custom drag and managers can track things, then we can be achieved, such as in the source view, drag a single or multiple data in target view, Then pass this in the custom manager for use in drag and drop operationsUIDragSessionIn thelocalContextProperties.

This is the second approach we’re using here.

Create the model data coordinator

Now that we’re talking about messing with a manager, let’s think about what the manager needs to do to accomplish the drag-and-drop and implement the model update:

  • Drag to find the corresponding data source and delete it.
  • The index path that stores the dragged data source.
  • Target data source, we can see where it is when we drag and drop the data source to the specified location.
  • Find the index path to which the drag-and-drop data source will be inserted.
  • Drag and drop the index path to which the item will be inserted
  • Here’s a scenario to illustrate, if we’re just moving or reordering, we’re going to useUICollectionViewTo provide theAPIDepending on whether the drag operation is moving or reordering, we want to have a drag that we can consult the manager about.
  • When all the steps are complete, we can update the source collection view.

We have requirements, now to implement the code, first set up an index manager:

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;
Copy the code

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];
}
Copy the code

After creating the index manager, we also need a ViewModel to manage the data source according to the index manager:

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;
Copy the code

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;
}
Copy the code

The end we come to realize this UICollectionView UICollectionViewDragDelegate, UICollectionViewDropDelegate proxy method:

#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:^(BOOLfinished) { }]; }}}#pragma mark - Collection View Drop Delegate
- (BOOL)collectionView:(UICollectionView *)collectionView
  canHandleDropSession:(id<UIDropSession>)session {
    
    returnsession.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;
}
Copy the code

This is a little bit similar to the UITableView usage, but a little different because it’s cross-view.

And this is just a Demo, so I didn’t think about it when I wrote it

The end result:

engineering

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


The last