史上第二走心的 iOS11-Drag & Drop 教程
話不多說,先上效果圖
普通view拖拽效果
TableView拖拽效果
CollectionView效果
muti-touch效果
多app互動
一.Tips:你必須要知道的概念
1. Drag 和 Drop 是什麼呢?
- 一種以圖形展現的方式把資料從一個 app 移動或拷貝到另一個 app(僅限iPad),或者在程式內部進行
- 充分利用了 iOS11 中新的檔案系統,只有在請求資料的時候才會去移動資料,而且保證只傳輸需要的資料
- 通過非同步的方式進行傳輸,這樣就不會阻塞runloop,從而保證在傳輸資料的時候使用者也有一個順暢的互動體驗
drag和drop的基本互動圖和支援的控制元件
2. 安全性:
- 拖拽複製的過程不像剪下板那樣,而是保證資料只對目標app可見
- 提供資料來源的app可以限制本身的資料來源只可在本 app 或者 公司組app 之間有許可權使用,當然也可以開放於所有 app,也支援企業使用者的管理配置
3. dragSession 的過程
Lift
:使用者長按 item,item 脫離螢幕Drag
:使用者開始拖拽,此時可進行 自定義檢視預覽、新增其他item新增內容、懸停進行導航(即iPad 中開啟別的app)Set Down
:此時使用者無非想進行兩種操作:取消拖拽 或者 在當前手指離開的位置對 item 進行 drop 操作Data Transfer
:目標app 會向 源app 進行資料請求- 這些都是圍繞互動這一概念構造的:即類似手勢識別器的概念,接收到使用者的操作後,進行view層級的改變
4. Others
- 需要給使用者提供 muti-touch 的使用,這一點也是為了支援企業使用者的管理配置(比如一個手指選中一段文字,長按其處於lifting狀態,另外一個手指選中若干張圖片,然後開啟郵件,把文字和圖片放進郵件,視覺反饋是及時的,動畫效果也很棒)
iPad 可實現的功能還是很豐富的
二、以CollectionView 為例,講一下整個拖拽的api使用情況
在API設計方面,分為兩個步驟:Drag 和 Drop,對應著兩套協議
UICollectionViewDragDelegate
和UICollectionViewDropDelegate
,因此在建立 CollectionView 的時候要增加以下程式碼:
- (void)buildCollectionView {
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:flowLayout];
[_collectionView registerClass:[WPFImageCollectionViewCell class] forCellWithReuseIdentifier:imageCellIdentifier];
_collectionView.delegate = self;
_collectionView.dataSource = self;
// 設定代理物件
_collectionView.dragDelegate = self;
_collectionView.dropDelegate = self;
_collectionView.dragInteractionEnabled = YES;
_collectionView.reorderingCadence = UICollectionViewReorderingCadenceImmediate;
_collectionView.springLoaded = YES;
_collectionView.backgroundColor = [UIColor whiteColor];
}
1. 建立CollectionView注意點總結:
-
dragInteractionEnabled
屬性在 iPad 上預設是YES,在 iPhone 預設是 NO,只有設定為 YES 才可以進行 drag 操作 -
reorderingCadence
(重排序節奏)可以調節集合檢視重排序的響應性。 是 CollectionView 獨有的屬性(相對於UITableView),因為 其獨有的二維網格的佈局,因此在重新排序的過程中有時候會發生元素迴流了,有時候只是移動到別的位置,不想要這樣的效果,就可以修改這個屬性改變其相應性UICollectionViewReorderingCadenceImmediate
:預設值,當開始移動的時候就立即迴流集合檢視佈局,可以理解為實時的重新排序UICollectionViewReorderingCadenceFast
:如果你快速移動,CollectionView 不會立即重新佈局,只有在停止移動的時候才會重新佈局UICollectionViewReorderingCadenceSlow
:停止移動再過一會兒才會開始迴流,重新佈局
-
springLoaded
:彈簧載入是一種導航和啟用控制元件的方式,在整個系統中,當處於 dragSession 的時候,只要懸浮在cell上面,就會高亮,然後就會啟用- UITableView 和 UICollectionView 都可以使用該方式載入,因為他們都遵守 UISpringLoadedInteractionSupporting 協議
- 當用戶在單元格使用彈性載入時,我們要選擇 CollectionView 或tableView 中的 item 或cell
- 使用
- (BOOL)collectionView:shouldSpringLoadItemAtIndexPath:withContext:
來自定義也是可以的
-
collectionView:itemsForAddingToDragSession: atIndexPath:
:該方法是muti-touch對應的方法- 當接收到新增item響應時,會呼叫該方法向已經存在的drag會話中新增item
- 如果需要,可以使用提供的點(在集合檢視的座標空間中)進行其他命中測試。
- 如果該方法未實現,或返回空陣列,則不會將任何 item 新增到拖動,手勢也會正常的響應
- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView itemsForAddingToDragSession:(id<UIDragSession>)session atIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point {
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];
UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];
return @[item];
}
再放一遍這個效果圖
2. UICollectionViewDragDelegate(初始化和自定義拖動方法)
collectionView: itemsForBeginningDragSession:atIndexPath:
提供一個 給定 indexPath 的可進行 drag 操作的 item(類似 hitTest: 方法周到該響應的view )如果返回 nil,則不會發生任何拖拽事件
由於是返回一個數組,因此可以根據自己的需求來實現該方法:比如拖拽一個item,就可以把該組的所有 item 放進 dragSession 中,右上角會有小藍圈圈顯示個數(但是這種情況下要對陣列進行重新排序,因為陣列中的最後一個元素會成為Lift 操作中的最上面的一個元素,排序後可以讓最先進入dragSession的item放在lift效果的最前面)
- (NSArray<UIDragItem *> *)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id<UIDragSession>)session atIndexPath:(NSIndexPath *)indexPath {
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.dataSource[indexPath.item]];
UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:itemProvider];
self.dragIndexPath = indexPath;
return @[item];
}
collectionView:dragPreviewParametersForItemAtIndexPath:
允許對從取消或返回到 CollectionView 的 item 使用自定義預覽,如果該方法沒有實現或者返回nil,那麼整個 cell 將用於預覽- UIDragPreviewParameters 有兩個屬性:
backgroundColor
設定背景顏色,因為有的檢視本身就是半透明的,新增背景色視覺效果更好visiblePath
設定檢視的可見區域,比如可以自定義為圓角矩形或圖中的某一塊區域等,但是要注意裁剪的Rect 在目標檢視中必須要有意義;該屬性也要標記一下center方便進行定位
- UIDragPreviewParameters 有兩個屬性:
裁剪圖中的某一塊區域
選取的區域也可以大於這張圖,實現新增相框的效果
再高階的功能可以實現目標區域內新增多個rect到dragSession
- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dragPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath {
// 可以在該方法內使用 貝塞爾曲線 對單元格的一個具體區域進行裁剪
UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
CGFloat previewLength = self.flowLayout.itemSize.width;
CGRect rect = CGRectMake(0, 0, previewLength, previewLength);
parameters.visiblePath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:5];
parameters.backgroundColor = [UIColor clearColor];
return parameters;
}
- 還有一些對於 drag 生命週期對應的回撥方法,可以在這些方法裡新增各種動畫效果
/* 當 lift animation 完成之後開始拖拽之前會呼叫該方法
* 該方法肯定會對應著 -collectionView:dragSessionDidEnd: 的呼叫
*/
- (void)collectionView:(UICollectionView *)collectionView dragSessionWillBegin:(id<UIDragSession>)session {
NSLog(@"dragSessionWillBegin --> drag 會話將要開始");
}
// 拖拽結束的時候會呼叫該方法
- (void)collectionView:(UICollectionView *)collectionView dragSessionDidEnd:(id<UIDragSession>)session {
NSLog(@"dragSessionDidEnd --> drag 會話已經結束");
}
當然也可以在這些方法裡面設定自定義的dragPreview,比如 iPad 中原生的通訊圖、地圖所展現的功能
在 dragSessionWillBegin 方法裡面自定義 preview 檢視
3. UICollectionViewDropDelegate(遷移資料和自定義釋放動畫)
Drop手勢的流程圖
collectionView:performDropWithCoordinator:
方法使用 dropCoordinator 去置頂如果處理當前 drop 會話的item 到指定的最終位置, 同時也會根據drop item返回的資料更新資料來源- 當用戶開始進行 drop 操作的時候會呼叫這個方法
- 如果該方法不做任何事,將會執行預設的動畫
- 注意:只有在這個方法中才可以請求到資料
請求的方式是非同步的,因此不要阻止資料的傳輸,如果阻止時間過長,就不清楚資料要多久才能到達,系統甚至可能會kill掉你的應用
- (void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
NSIndexPath *destinationIndexPath = coordinator.destinationIndexPath;
UIDragItem *dragItem = coordinator.items.firstObject.dragItem;
UIImage *image = self.dataSource[self.dragIndexPath.row];
// 如果開始拖拽的 indexPath 和 要釋放的目標 indexPath 一致,就不做處理
if (self.dragIndexPath.section == destinationIndexPath.section && self.dragIndexPath.row == destinationIndexPath.row) {
return;
}
// 更新 CollectionView
[collectionView performBatchUpdates:^{
// 目標 cell 換位置
[self.dataSource removeObjectAtIndex:self.dragIndexPath.item];
[self.dataSource insertObject:image atIndex:destinationIndexPath.item];
[collectionView moveItemAtIndexPath:self.dragIndexPath toIndexPath:destinationIndexPath];
} completion:^(BOOL finished) {
}];
[coordinator dropItem:dragItem toItemAtIndexPath:destinationIndexPath];
}
collectionView: dropSessionDidUpdate: withDestinationIndexPath:
該方法是提供釋放方案的方法,雖然是optional,但是最好實現- 當 跟蹤 drop 行為在 tableView 空間座標區域內部時會頻繁呼叫(因此要儘量減少這個方法的工作量,否則幀率就會降低)
- 當drop手勢在某個section末端的時候,傳遞的目標索引路徑還不存在(此時 indexPath 等於 該 section 的行數),這時候會追加到該section 的末尾
- 在某些情況下,目標索引路徑可能為空(比如拖到一個沒有cell的空白區域)
- 請注意,在某些情況下,你的建議可能不被系統所允許,此時系統將執行不同的建議
- 你可以通過 -[session locationInView:] 做你自己的命中測試
UICollectionViewDropIntent
對應的三個列舉值UICollectionViewDropIntentUnspecified
將會接收drop,但是具體的位置要稍後才能確定;不會開啟一個缺口,可以通過新增視覺效果給使用者傳達這一資訊UICollectionViewDropIntentInsertAtDestinationIndexPathdrop
將會被插入到目標索引中;將會開啟一個缺口,模擬最後釋放後的佈局UICollectionViewDropIntentInsertIntoDestinationIndexPathdrop
將會釋放在目標索引路徑,比如該cell是一個容器(集合),此時不會像