玩轉iOS開發:iOS 11 新特性《高階拖放》

CainLuo發表於2019-03-02

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

最後

碼字很費腦, 看官賞點飯錢可好
微信
支付寶

相關文章