玩转iOS开发:iOS 11 新特性《高级拖放》

2,617 阅读5分钟

文章分享至我的个人技术博客: https://cainluo.github.io/15130820516379.html

在这之前, 我们已经知道了iOS 11的拖拽功能, 也试过在单个视图里拖拽和跨视图的拖拽, 但好像和我们在看WWDC 2017里的不太一样, 这次我们把最后的一点讲完, 就是跨App的拖拽.

如果没有了解过之前的文章, 那么可以去看看之前的文章:

玩转iOS开发:iOS 11 新特性《UIKit新特性的基本认识》 玩转iOS开发:iOS 11 新特性《UICollectionView的拖拽》

转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.

UIDragInteractionDelegate和UIDropInteractionDelegate代理

这次重点说的是两个代理协议UIDragInteractionDelegateUIDropInteractionDelegate.

这两个协议里分别定义了拖放的行为, 它们的核心功能跟UICollectionViewDragDelegateUICollectionViewDropDelegate类似, 只不过提供了更多的自定义选项, 特别是在动画和安全性方面.

当在拖动的源App开始拖动, 就会生成一个拖动的会话, 用来监督拖动的对象, 拖动到目标的App时, 就会生成一个放置的会话, 而UIDragSessionUIDropSession的目的是为拖放代理所提供的拖动对象的信心, 无论是实际的数据还是它们的位置都有.

为了可以接受拖动, 我们需要在源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, NSItemProviderWritingNSCoding三个协议.

并且对应的去实现它们各自的方法:

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: kUTTypePNGkUTTypePlainText是属于MobileCoreServices框架里的, 并且是CFString类型, 如果要使用, 记得先导入<MobileCoreServices/MobileCoreServices.h>并且转换成NSString类型, 这些都是iOS系统所提供的, 还有更多的类型可以到UICoreTypes.h头文件里查看.

最终效果

1

2

总结

拖放的内容讲到这里基本上就已经结束了, 但别以为就完了咯, 还有很多东西需要我们去学习下面就放几个视频地址给大家了解更多:

第三方的视频:

前面我们写了很多关于com.xxx.xxx的东西, 其实叫做UTI, 下面有两篇关于UTI的官方文章:

工程

https://github.com/CainRun/iOS-11-Characteristic/tree/master/5.AdvancedDragAndDrop

最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝