文章分享至個人我的技術博客: 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