在使用PC進行操做時,你必定遇到過這樣的場景,能夠將圖片直接拖入聊天軟件進行發送,能夠將文檔、音樂、視頻文件等文件拖入相應應用程序直接進行使用。這種拖拽操做交互極大的方便了電腦的使用。在iOS11中,你能夠在iPhone或iPad上構建這種交互體驗!ios
說在前面的話:編程
拖拽操做在iPad上是支持跨應用程序的,你能夠從一個應用中拖取項目,經過Home鍵回到主界面而且打開另外一個應用程序,而後將被拖拽的項目傳遞給這個應用程序中。在iPhone上,拖拽操做只支持當前應用程序內,你能夠將某個元素從一個界面拖拽到另外一個,這種維度的操做能夠給設計人員更大的靈活性。數組
拖拽操做被設計成系統管理,開發者不須要爲App申請特殊的用戶權限。 session
對於拖拽操做,至少要有兩個組件,一個組件做爲拖拽源用來提供數據,一個組件做爲拖拽目的用來接收數據,當前,同一個組件既能夠是拖拽源也能夠是拖拽目的。首先咱們先來看拖拽源,在UIKit框架中,iOS11默認實現了一些組件能夠做爲拖拽源, 例如UITextField、UITextView、UITableView和UICollectionView等。文本組件默認支持拖拽操做進行文本的傳遞,對於列表組件則默認支持元素的拖拽。例如,在UITextField選中的文案中進行拖拽,能夠將文字拖拽出來,效果以下圖:app
任意的UIView組件均可以做爲拖拽源,讓其成爲拖拽源其實也十分簡單,只須要3步:框架
1.建立一個UIDragInteraction行爲對象。ide
2.設置UIDragInteraction對象的代理並實現相應方法。函數
3.將UIDragInteraction對象添加到指定View上。動畫
最簡單的可拖拽組件的建立示例代碼以下:atom
- (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.dragView]; } //建立View -(UIView *)dragView{ if (!_dragView) { _dragView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; _dragView.backgroundColor = [UIColor redColor]; [_dragView addInteraction:self.dragInteraction]; } return _dragView; } //建立拖拽行爲對象 -(UIDragInteraction *)dragInteraction{ if (!_dragInteraction) { _dragInteraction = [[UIDragInteraction alloc]initWithDelegate:self]; //要設置可用 注意!!! [_dragInteraction setEnabled:YES]; } return _dragInteraction; } //實現提供數據的代理方法 - (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction itemsForBeginningSession:(id<UIDragSession>)session{ //數據提供者 NSItemProvider * provider = [[NSItemProvider alloc]initWithObject:@"Hello World"]; UIDragItem * item = [[UIDragItem alloc]initWithItemProvider:provider]; return @[item]; }
上面的dragInteraction:代理方法用來提供要傳遞的數據,傳遞的數據必須遵照相應的承諾協議,後面會給你們介紹,這裏只是簡單返回了一個字符串數據Hello World,運行工程,你能夠試驗下,能夠直接將咱們自定義的視圖拖拽進UITextField並在其中顯示Hello World。
全部能夠接收拖拽行爲的組件都必須經過這個類實現,這個類中屬性意義列舉以下:
//初始化方法 - (instancetype)initWithDelegate:(id<UIDragInteractionDelegate>)delegate; //代理 @property (nonatomic, nullable, readonly, weak) id<UIDragInteractionDelegate> delegate; //是否支持多種手勢都接收響應 @property (nonatomic) BOOL allowsSimultaneousRecognitionDuringLift; //設置是否有效 @property (nonatomic, getter=isEnabled) BOOL enabled; //獲取默認是否有效 不一樣的設備這個值將有所區別 @property (class, nonatomic, readonly, getter=isEnabledByDefault) BOOL enabledByDefault;
UIDragInteractionDelegate用來處理拖拽源的行爲與數據。其中定義了一個必須實現的方法和許多可選實現的方法。解析以下:
/* 這個方法是必須實現的用來返回拖拽源提供的數據 須要注意,這個函數須要返回一個數組,數組中能夠有多個數據源 若是返回空數組,則拖拽行爲不會開始 */ - (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction itemsForBeginningSession:(id<UIDragSession>)session; /* 這個方法用來自定義拖拽效果的預覽視圖 關於預覽視圖,後面會介紹 須要注意,系統默認會提供一個預覽視圖,不實現這個方法便是使用系統默認的 若是返回nil,則會去除預覽動畫 */ - (nullable UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction previewForLiftingItem:(UIDragItem *)item session:(id<UIDragSession>)session; /* 拖拽動畫即將開始時會調用此函數 */ - (void)dragInteraction:(UIDragInteraction *)interaction willAnimateLiftWithAnimator:(id<UIDragAnimating>)animator session:(id<UIDragSession>)session; //拖拽行爲會話即將開始時調用的方法 - (void)dragInteraction:(UIDragInteraction *)interaction sessionWillBegin:(id<UIDragSession>)session; //這個方法設置數據的防止是否容許數據的 移動操做,須要注意,這個只有在app內有效,跨app的操做會老是複製數據 - (BOOL)dragInteraction:(UIDragInteraction *)interaction sessionAllowsMoveOperation:(id<UIDragSession>)session; //設置是否容許跨應用程序進行拖拽 ipad - (BOOL)dragInteraction:(UIDragInteraction *)interaction sessionIsRestrictedToDraggingApplication:(id<UIDragSession>)session; //設置預覽視圖是否顯示原始大小 - (BOOL)dragInteraction:(UIDragInteraction *)interaction prefersFullSizePreviewsForSession:(id<UIDragSession>)session; /* 當拖拽源被移動時調用,能夠用以下方法獲取其座標 NSLog(@"%f,%f",[session locationInView:self.view].x,[session locationInView:self.view].y); */ - (void)dragInteraction:(UIDragInteraction *)interaction sessionDidMove:(id<UIDragSession>)session; //拖拽行爲將要結束時調用 - (void)dragInteraction:(UIDragInteraction *)interaction session:(id<UIDragSession>)session willEndWithOperation:(UIDropOperation)operation; //拖拽行爲已經結束時調用 - (void)dragInteraction:(UIDragInteraction *)interaction session:(id<UIDragSession>)session didEndWithOperation:(UIDropOperation)operation; //拖拽源進行了放置操做後調用 - (void)dragInteraction:(UIDragInteraction *)interaction sessionDidTransferItems:(id<UIDragSession>)session; //設置拖拽動做取消的視圖動畫 返回nil則消除動畫 -(nullable UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction previewForCancellingItem:(UIDragItem *)item withDefault:(UITargetedDragPreview *)defaultPreview; //拖拽動做即將取消時調用的方法 - (void)dragInteraction:(UIDragInteraction *)interaction item:(UIDragItem *)item willAnimateCancelWithAnimator:(id<UIDragAnimating>)animator; //設置是否容許向拖拽中的項目添加數據 /* 能夠返回數據載體數組 當拖拽過程當中 點擊可拖拽的組件時會觸發 */ - (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction itemsForAddingToSession:(id<UIDragSession>)session withTouchAtPoint:(CGPoint)point; //設置容許進行拖拽中追加數據的拖拽行爲會話 - (nullable id<UIDragSession>)dragInteraction:(UIDragInteraction *)interaction sessionForAddingItems:(NSArray<id<UIDragSession>> *)sessions withTouchAtPoint:(CGPoint)point; //將要向拖拽組件中追加數據時調用 - (void)dragInteraction:(UIDragInteraction *)interaction session:(id<UIDragSession>)session willAddItems:(NSArray<UIDragItem *> *)items forInteraction:(UIDragInteraction *)addingInteraction;
上面列舉的協議方法中有關聯到其餘許多iOS11中新增的類,後面會一一介紹。其實,完成了以上內容的瞭解,你就已經能夠徹底爲所欲爲的定製拖拽源組件了。
拖拽源是數據的提供者,放置目的地就是數據的接收者。前面咱們也實驗過,將自定義的拖拽源拖拽進UITextField後,文本框中會自動填充咱們提供的文本數據。一樣,對於任何自定義的UIView視圖,咱們也可讓其成爲放置目的地,須要完成以下3步:
1.建立一個UIDropInteraction行爲對象。
2.設置UIDropInteraction對象的代理並實現協議方法。
3.將其添加到自定義的視圖中。
例如,咱們將自定義的UILabel組件用來顯示拖拽的文案:
//添加視圖 - (void)viewDidLoad { [super viewDidLoad]; //有關拖拽源的代碼 前面已經列舉過 這裏再也不重複 [self.view addSubview:self.dragView]; [self.view addSubview:self.dropLabel]; } -(UILabel *)dropLabel{ if (!_dropLabel) { _dropLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 300, 300, 30)]; _dropLabel.backgroundColor = [UIColor greenColor]; _dropLabel.userInteractionEnabled = YES; [_dropLabel addInteraction:self.dropInteraction]; } return _dropLabel; } //放置目的地行爲對象 -(UIDropInteraction*)dropInteraction{ if (!_dropInteraction) { _dropInteraction = [[UIDropInteraction alloc]initWithDelegate:self]; } return _dropInteraction; } //這個方法返回是否響應此放置目的地的放置請求 -(BOOL)dropInteraction:(UIDropInteraction *)interaction canHandleSession:(id<UIDropSession>)session{ return YES; } //設置以何種方式響應拖放會話行爲 -(UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id<UIDropSession>)session{ return [[UIDropProposal alloc]initWithDropOperation:UIDropOperationCopy]; } //已經應用拖放行爲後執行的操做 -(void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id<UIDropSession>)session{ [session loadObjectsOfClass:[NSString class] completion:^(NSArray<__kindof id<NSItemProviderReading>> * _Nonnull objects) { self.dropLabel.text = objects.firstObject; }]; }
上面的代碼將咱們自定義的拖拽源提供的Hello World拖放進了UILabel組件中。
與UIDragInteraction類相似,這個類的做用是讓組件有相應放置操做的能力。其中屬性以下:
//初始化方法 - (instancetype)initWithDelegate:(id<UIDropInteractionDelegate>)delegate; //代理對象 @property (nonatomic, nullable, readonly, weak) id<UIDropInteractionDelegate> delegate; //是否容許多個交互行爲 @property (nonatomic, assign) BOOL allowsSimultaneousDropSessions;
UIDropInteractionDelegate協議中所定義的方法所有是可選實現的,其用來處理用戶放置交互行爲。
//放置行爲即將響應時觸發的方法 返回值肯定是否響應這次行爲 - (BOOL)dropInteraction:(UIDropInteraction *)interaction canHandleSession:(id<UIDropSession>)session; //當上面的協議方法返回YES時會接着調用這個函數 - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidEnter:(id<UIDropSession>)session; //將要處理數據時回調的方法 /* 當數據源數據添加時,這個方法也會被從新調用 這個函數須要返回一個處理行爲方式UIDropProposal對象,這個咱們後面再說 */ - (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id<UIDropSession>)session; //放置行爲相應結束的時候會調用此方法 - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidExit:(id<UIDropSession>)session; //這個方法當用戶進行放置時會調用,能夠從session中獲取被傳遞的數據 - (void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id<UIDropSession>)session; //放置動畫完成後會調用這個方法 - (void)dropInteraction:(UIDropInteraction *)interaction concludeDrop:(id<UIDropSession>)session; //整個拖放行爲結束後會調用 - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidEnd:(id<UIDropSession>)session; //下面這些方法用來自定義放置動畫 //設置放置預覽動畫 - (nullable UITargetedDragPreview *)dropInteraction:(UIDropInteraction *)interaction previewForDroppingItem:(UIDragItem *)item withDefault:(UITargetedDragPreview *)defaultPreview; //這個函數每當有一個拖拽數據項放入時都會調用一次 能夠進行動畫 - (void)dropInteraction:(UIDropInteraction *)interaction item:(UIDragItem *)item willAnimateDropWithAnimator:(id<UIDragAnimating>)animator;
須要注意,UIDropProposal類用來進行處理回執,屬性方法解析以下:
//初始化方法 /* typedef NS_ENUM(NSUInteger, UIDropOperation) { //取消此次行爲 UIDropOperationCancel = 0, //拒絕行爲 UIDropOperationForbidden = 1, //接收拷貝數據 UIDropOperationCopy = 2, //接收移動數據 UIDropOperationMove = 3, } */ - (instancetype)initWithDropOperation:(UIDropOperation)operation; //處理方式 @property (nonatomic, readonly) UIDropOperation operation; //精準定位 @property (nonatomic, getter=isPrecise) BOOL precise; //設置是否展現完整的預覽尺寸 @property (nonatomic) BOOL prefersFullSizePreview;
UIDragItem類用來承載要傳遞的數據。其經過NSItemProvider類來進行構建,傳遞的數據類型是有嚴格規定的,必須遵照必定的協議,系統的NSString,NSAttributeString,NSURL,UIColor和UIImage是默認支持的,你能夠直接傳遞這些數據。
UIDragItem中提供的屬性方法:
//初始化方法 - (instancetype)initWithItemProvider:(NSItemProvider *)itemProvider; //數據提供者實例 @property (nonatomic, readonly) __kindof NSItemProvider *itemProvider; //用來傳遞一些額外的關聯信息 @property (nonatomic, strong, nullable) id localObject; //用來自定義每一個item添加時的預覽動畫 @property (nonatomic, copy, nullable) UIDragPreview * _Nullable (^previewProvider)(void);
在與拖拽交互相關的接口中,這兩個是面向協議編程的絕佳範例,首先在UIKit框架中只定義了這兩個協議,而並無相關的實現類,在拖拽行爲的相關回調接口中,不少id類型的參數都遵照了這個協議,咱們無需知道是哪一個類實現的,直接進行使用便可:
UIDropSession:
//繼承於UIDragDropSession(提供基礎數據), NSProgressReporting(提供數據讀取進度) @protocol UIDropSession <UIDragDropSession, NSProgressReporting> //原始的dragSesstion會話 若是是跨應用的 則爲nil @property (nonatomic, readonly, nullable) id<UIDragSession> localDragSession; //設置進度風格 /* typedef NS_ENUM(NSUInteger, UIDropSessionProgressIndicatorStyle) { UIDropSessionProgressIndicatorStyleNone, // 無 UIDropSessionProgressIndicatorStyleDefault, // 默認的 } API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos); */ @property (nonatomic) UIDropSessionProgressIndicatorStyle progressIndicatorStyle; //進行數據的加載 - (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id<NSItemProviderReading>> *objects))completion; @end
UIDragSession:
API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos) @protocol UIDragSession <UIDragDropSession> //設置要傳遞的額外信息 只有在同個APP內可見 @property (nonatomic, strong, nullable) id localContext; @end
UIDragDropSession:
//傳遞的數據數組 @property (nonatomic, readonly) NSArray<UIDragItem *> *items; //當前操做行爲的座標 - (CGPoint)locationInView:(UIView *)view; //這次行爲是否容許移動操做 @property (nonatomic, readonly) BOOL allowsMoveOperation; //是否支持應用程序層面的拖拽 @property (nonatomic, readonly, getter=isRestrictedToDraggingApplication) BOOL restrictedToDraggingApplication; //驗證傳遞的數據是否支持某個數據類型協議 - (BOOL)hasItemsConformingToTypeIdentifiers:(NSArray<NSString *> *)typeIdentifiers; //驗證傳遞的數據是否能夠加載某個類 - (BOOL)canLoadObjectsOfClass:(Class<NSItemProviderReading>)aClass;
UITargetedDragPreview專門用來處理拖放交互過程當中的動畫與預覽視圖。方法解析以下:
//建立一個預覽對象 /* view:要建立的預覽視圖 須要注意,這個視圖必須在window上 param:配置參數 target:容器視圖,用來展現預覽,通常設置爲view的父視圖 */ - (instancetype)initWithView:(UIView *)view parameters:(UIDragPreviewParameters *)parameters target:(UIDragPreviewTarget *)target; //同上 -(instancetype)initWithView:(UIView *)view parameters:(UIDragPreviewParameters *)parameters; //同上 - (instancetype)initWithView:(UIView *)view; //動畫承載者 @property (nonatomic, readonly) UIDragPreviewTarget* target; //動畫視圖 @property (nonatomic, readonly) UIView *view; //配置參數 @property (nonatomic, readonly, copy) UIDragPreviewParameters *parameters; //尺寸 @property (nonatomic, readonly) CGSize size; //返回新的對象 - (UITargetedDragPreview *)retargetedPreviewWithTarget:(UIDragPreviewTarget *)newTarget;
UIDragPreviewTarget主要用來設置動畫的起始視圖與結束時迴歸的視圖,其中屬性方法以下:
/* 初始化方法 container:必須是在window上的view center:動畫起點與終點 transform:進行變換 */ - (instancetype)initWithContainer:(UIView *)container center:(CGPoint)center transform:(CGAffineTransform)transform; //同上 - (instancetype)initWithContainer:(UIView *)container center:(CGPoint)center; //對應屬性 @property (nonatomic, readonly) UIView *container; @property (nonatomic, readonly) CGPoint center; @property (nonatomic, readonly) CGAffineTransform transform;
UIDragPreviewParameters用來進行拖拽動畫的配置,解析以下:
//構造方法並設置路徑矩形 - (instancetype)initWithTextLineRects:(NSArray<NSValue /* CGRect */ *> *)textLineRects; //顯示的路徑 @property (nonatomic, copy, nullable) UIBezierPath *visiblePath; //背景色 @property (nonatomic, copy, null_resettable) UIColor *backgroundColor;
咱們可使用任意自定義的視圖來展示這個預覽動畫,以下圖所示:
本篇文章到這裏,其實基本的內容都已經說完了,雖然比較詳細,也可能不免冗餘,若是你耐着性子看到了這裏,那麼我首先欽佩你的毅力而且感謝你的耐心。其實,拖拽交互若是進行只能對系統的提供的數據類型進行操做則應用就侷限太多。試想一下,若是咱們能夠經過拖拽商品來進行購買,拖拽聯繫人來進行發送,或者在遊戲中,拖拽進行卡片的融合,裝備的提煉等等這種交互操做是否是會很暢快。最後,咱們就來看看如何讓自定義的數據類型支持拖拽操做。
首先你須要關注兩個協議,NSItemProviderWriting與NSItemProviderReading。Writing協議用來讓數據支持提供給數據源,Reading協議讓數據支持從數據源讀出,用自定義的Person類爲例:
#import <Foundation/Foundation.h> //遵照協議 @interface Person : NSObject<NSItemProviderWriting,NSItemProviderReading> //自定義內容 @property(nonatomic,strong)NSString * name; @property(nonatomic,assign)NSUInteger age; @end //.m文件 @implementation Person //數據歸檔 - (nullable NSProgress *)loadDataWithTypeIdentifier:(NSString *)typeIdentifier forItemProviderCompletionHandler:(void (^)(NSData * _Nullable data, NSError * _Nullable error))completionHandler{ NSProgress * pro = [NSProgress new]; NSData * data = [NSKeyedArchiver archivedDataWithRootObject:self]; completionHandler(data,nil); return pro; } +(NSItemProviderRepresentationVisibility)itemProviderVisibilityForRepresentationWithTypeIdentifier:(NSString *)typeIdentifier{ return NSItemProviderRepresentationVisibilityAll; } - (NSItemProviderRepresentationVisibility)itemProviderVisibilityForRepresentationWithTypeIdentifier:(NSString *)typeIdentifier{ return NSItemProviderRepresentationVisibilityAll; } //提供一個標識符 +(NSArray<NSString *> *)writableTypeIdentifiersForItemProvider{ return @[@"object"]; } -(NSArray<NSString *> *)writableTypeIdentifiersForItemProvider{ return @[@"object"]; } - (instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { self.name = [coder decodeObjectForKey:@"name"]; self.age = [coder decodeIntegerForKey:@"age"]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder{ [aCoder encodeObject:self.name forKey:@"name"]; [aCoder encodeInteger:self.age forKey:@"age"]; } //這兩個是讀協議 +(NSArray<NSString *> *)readableTypeIdentifiersForItemProvider{ return @[@"object"]; } //解歸檔返回 + (nullable instancetype)objectWithItemProviderData:(NSData *)data typeIdentifier:(NSString *)typeIdentifier error:(NSError **)outError{ Person * p = [NSKeyedUnarchiver unarchiveObjectWithData:data]; return p; } @end
須要注意,在拖放行爲讀取數據時的類型要對應,以下:
-(void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id<UIDropSession>)session{ NSLog(@"%@",session.items.lastObject.localObject); [session loadObjectsOfClass:[Person class] completion:^(NSArray<__kindof id<NSItemProviderReading>> * _Nonnull objects) { self.dropLabel.text = ((Person*)objects.firstObject).name; }]; }
寫了這麼多,不免有疏漏與錯誤,歡迎指導交流