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