文章分享至我的個人技術部落格: https://cainluo.github.io/15130820516379.html
在這之前, 我們已經知道了iOS 11
的拖拽功能, 也試過在單個檢視裡拖拽和跨檢視的拖拽, 但好像和我們在看WWDC 2017
裡的不太一樣, 這次我們把最後的一點講完, 就是跨App
的拖拽.
如果沒有了解過之前的文章, 那麼可以去看看之前的文章:
玩轉iOS開發:iOS 11 新特性《UIKit新特性的基本認識》
玩轉iOS開發:iOS 11 新特性《UICollectionView的拖拽》
轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.
UIDragInteractionDelegate和UIDropInteractionDelegate代理
這次重點說的是兩個代理協議UIDragInteractionDelegate
和UIDropInteractionDelegate
.
這兩個協議裡分別定義了拖放的行為, 它們的核心功能跟UICollectionViewDragDelegate
和UICollectionViewDropDelegate
類似, 只不過提供了更多的自定義選項, 特別是在動畫和安全性方面.
當在拖動的源App
開始拖動, 就會生成一個拖動的會話, 用來監督拖動的物件, 拖動到目標的App
時, 就會生成一個放置的會話, 而UIDragSession
和UIDropSession
的目的是為拖放代理所提供的拖動物件的信心, 無論是實際的資料還是它們的位置都有.
為了可以接受拖動, 我們需要在源App
裡有一個UIDragInteraction
並且配置好一個UIDragInteractionDelegate
, 這時候我們在檢視上拖動物件時, 委託就會返回一個或者多個的UIDragItem
物件, 每個UIDragItem
都會使用NSItemProvider
來共享被拖動的物件.
而在拖放時, 我們就需要有一個包含UIDropInteraction
的檢視, 它會諮詢對應的UIDropInteractionDelegate
是否可以處理拖放操作, 最後代理可以從拖放會話中拿到UIDragItem
物件, 並使用NSItemProvider
來載入對應的資料.
建立源應用程式
剛剛就把大致的思路講完了, 現在我們來直接搗鼓一下源App
.
建立源應用程式工程
這裡我們建立一個源程式, 配置一個UIDragInteraction
並且實現UIDragInteractionDelegate
協議.
UI
介面這裡就不展示了, 就一個UILabel
和一個UIImageView
, 配置好UI
之後, 我們來搗鼓其他東西:
配置UIDragInteraction
在啟動拖放之前, 我們需要把UIImageView
的某個屬性userInteractionEnabled
設定為YES
.
self.imageView.userInteractionEnabled = YES;
複製程式碼
新增UIDragInteraction
:
UIDragInteraction *dragInteraction = [[UIDragInteraction alloc] initWithDelegate:self];
[self.view addInteraction:dragInteraction];
複製程式碼
實現資料共享的代理方法:
- (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction
itemsForBeginningSession:(id<UIDragSession>)session {
if (!self.imageModel) {
return @[];
}
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.imageModel];
UIDragItem *dragItem = [[UIDragItem alloc] initWithItemProvider:itemProvider];
return @[dragItem];
}
複製程式碼
設定一下拖動時預覽的頁面:
- (UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction
previewForLiftingItem:(UIDragItem *)item
session:(id<UIDragSession>)session {
UIView *dragView = interaction.view;
if (!dragView && !self.imageModel) {
return [[UITargetedDragPreview alloc] initWithView:interaction.view];
}
ImageDragView *imageDragView = [[ImageDragView alloc] initWithTitle:self.imageModel.title
image:self.imageModel.image];
UIDragPreviewParameters *dragPreviewParameters = [[UIDragPreviewParameters alloc] init];
dragPreviewParameters.visiblePath = [UIBezierPath bezierPathWithRoundedRect:imageDragView.bounds
cornerRadius:20];
CGPoint dragPoint = [session locationInView:dragView];
UIDragPreviewTarget *dragPreviewTarget = [[UIDragPreviewTarget alloc] initWithContainer:dragView
center:dragPoint];
return [[UITargetedDragPreview alloc] initWithView:imageDragView
parameters:dragPreviewParameters
target:dragPreviewTarget];
}
- (UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction
previewForCancellingItem:(UIDragItem *)item
withDefault:(UITargetedDragPreview *)defaultPreview {
UIView *superView = self.imageView.superview;
if (!superView) {
return defaultPreview;
}
UIDragPreviewTarget *dragPreviewTarget = [[UIDragPreviewTarget alloc] initWithContainer:superView
center:self.imageView.center];
return [[UITargetedDragPreview alloc] initWithView:self.imageView
parameters:[[UIDragPreviewParameters alloc] init]
target:dragPreviewTarget];
}
複製程式碼
最後, 我們來設定一下是否要限制這個拖放會話, 如果設定為YES
, 系統就會取消掉我們的拖放會話, 所以這裡我們要設定為NO
:
- (BOOL)dragInteraction:(UIDragInteraction *)interaction
sessionIsRestrictedToDraggingApplication:(id<UIDragSession>)session {
return NO;
}
複製程式碼
這樣子源程式就基本上可以了.
建立目標App
在目標App裡, 我們也有對應的內容, 但多了一個清除內容的按鈕, 這裡我們也要設定一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.clearButton.springLoaded = YES;
UIDropInteraction *dropInteraction = [[UIDropInteraction alloc] initWithDelegate:self];
[self.view addInteraction:dropInteraction];
[self display];
}
- (IBAction)clearAction:(UIButton *)sender {
self.imageModel = nil;
self.titleLabel.text = @"";
[self display];
}
- (void)display {
if (!self.imageModel) {
self.imageView.image = nil;
self.titleLabel.text = @"";
return;
}
self.imageView.image = self.imageModel.image;
self.titleLabel.text = self.imageModel.title;
}
複製程式碼
做好前期設定之後, 我們就需要去實現對應的UIDropInteractionDelegate
的代理方法:
- (BOOL)dropInteraction:(UIDropInteraction *)interaction
canHandleSession:(id<UIDropSession>)session {
return [session canLoadObjectsOfClass:[ImageModel class]];
}
- (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction
sessionDidUpdate:(id<UIDropSession>)session {
return [[UIDropProposal alloc] initWithDropOperation:UIDropOperationCopy];
}
- (void)dropInteraction:(UIDropInteraction *)interaction
performDrop:(id<UIDropSession>)session {
UIDragItem *dropItem = session.items.lastObject;
if (!dropItem) {
return;
}
session.progressIndicatorStyle = UIDropSessionProgressIndicatorStyleNone;
self.progress = [dropItem.itemProvider loadObjectOfClass:[ImageModel class]
completionHandler:^(id<NSItemProviderReading> _Nullable object, NSError * _Nullable error) {
self.imageModel = (ImageModel *)object;
if (!self.imageModel) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self display];
[self.loadingView removeFromSuperview];
self.loadingView = nil;
});
}];
}
- (void)dropInteraction:(UIDropInteraction *)interaction
item:(UIDragItem *)item
willAnimateDropWithAnimator:(id<UIDragAnimating>)animator {
NSProgress *progress = self.progress;
UIView *interactionView = interaction.view;
if (!interactionView || !progress) {
return;
}
self.loadingView = [[LoadingView alloc] initWithFrame:interactionView.bounds
progress:progress];
[interactionView addSubview:self.loadingView];
}
複製程式碼
這裡為了更好的使用者體驗, 新增了一個載入進度的檢視LoadingView
, 程式碼的話, 可以自行到工程裡尋找.
配置公共資料模型
剛剛我們已經把源應用和目標應用都寫好了, 這裡我們需要重點提一下這個共享的資料模型ImageModel
.
在這裡面, 我們要去遵守NSItemProviderReading
, NSItemProviderWriting
和NSCoding
三個協議.
並且對應的去實現它們各自的方法:
NSCoding
協議方法:
#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
UIImage *image = [UIImage imageWithData:[aDecoder decodeObjectForKey:@"image"]];
NSString *title = [aDecoder decodeObjectForKey:@"title"];
return [self initWithTitle:title image:image];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:UIImagePNGRepresentation(self.image)
forKey:@"image"];
[aCoder encodeObject:self.title
forKey:@"title"];
}
複製程式碼
NSItemProviderReading
協議方法:
+ (nullable instancetype)objectWithItemProviderData:(NSData *)data
typeIdentifier:(NSString *)typeIdentifier
error:(NSError **)outError {
if ([typeIdentifier isEqualToString:IMAGE_TYPE]) {
ImageModel *imageModel = [NSKeyedUnarchiver unarchiveObjectWithData:data];
return [[self alloc] initWithImageModel:imageModel];
}
return nil;
}
+ (NSArray<NSString *> *)readableTypeIdentifiersForItemProvider {
return @[IMAGE_TYPE];
}
複製程式碼
NSItemProviderWriting
協議方法:
- (nullable NSProgress *)loadDataWithTypeIdentifier:(NSString *)typeIdentifier
forItemProviderCompletionHandler:(void (^)(NSData * _Nullable data, NSError * _Nullable error))completionHandler {
if ([typeIdentifier isEqualToString:(__bridge NSString *)kUTTypePNG]) {
NSData *imageData = UIImagePNGRepresentation(self.image);
if (imageData) {
completionHandler(imageData, nil);
} else {
completionHandler(nil, nil);
}
} else if ([typeIdentifier isEqualToString:(__bridge NSString *)kUTTypePlainText]) {
completionHandler([self.title dataUsingEncoding:NSUTF8StringEncoding], nil);
} else if ([typeIdentifier isEqualToString:IMAGE_TYPE]) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
completionHandler(data, nil);
}
return nil;
}
+ (NSArray<NSString *> *)writableTypeIdentifiersForItemProvider {
return @[IMAGE_TYPE, (__bridge NSString *)kUTTypePNG, (__bridge NSString *)kUTTypePlainText];
}
複製程式碼
這樣子就可以了, 在Demo
裡我並沒有把這個公共的資料模型打包成Framework
, 但如果是在實際專案中, 建議打包好成對應的Framework
.
PS:
kUTTypePNG
和kUTTypePlainText
是屬於MobileCoreServices
框架裡的, 並且是CFString
型別, 如果要使用, 記得先匯入<MobileCoreServices/MobileCoreServices.h>
並且轉換成NSString
型別, 這些都是iOS
系統所提供的, 還有更多的型別可以到UICoreTypes.h
標頭檔案裡檢視.
最終效果
總結
拖放的內容講到這裡基本上就已經結束了, 但別以為就完了咯, 還有很多東西需要我們去學習下面就放幾個視訊地址給大家瞭解更多:
- 介紹拖放: apple.co/2vO46Q4
- 掌握拖放: apple.co/2vOhvYA
- 拖放資料傳輸: apple.co/2tszCCm
第三方的視訊:
- 多個資料表示和自定義檢視: bit.ly/2eGhceO
- UITableView和UICollectionView: bit.ly/2unOBAH
前面我們寫了很多關於com.xxx.xxx
的東西, 其實叫做UTI
, 下面有兩篇關於UTI
的官方文章:
工程
https://github.com/CainRun/iOS-11-Characteristic/tree/master/5.AdvancedDragAndDrop