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 through
ListController
Manage separately. ListController
It’s basically showing oneUICollectionView
And we drag and dropListController
In 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:
- If you drag a single piece of data between views of different classes, such as custom
UIView
andUICollectionView
We can go throughlocalObject
This property appends the model object toUIDragItem
When we receive a drag and drop, we can pass it through the drag and drop managerlocalObject
Retrieves model objects in. - Drag and drop one or more data from two or more collection class views (e.g
UITableView
andUITableView
.UICollectionView
andUICollectionView
.UITableView
andUICollectionView
), 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 operationsUIDragSession
In thelocalContext
Properties.
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 use
UICollectionView
To provide theAPI
Depending 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