iOS11新特性:新增拖拽交互体验
一、引言
在使用PC进行操作时,你一定遇到过这样的场景,可以将图片直接拖入聊天软件进行发送,可以将文档、音乐、视频文件等文件拖入相应应用程序直接进行使用。这种拖拽操作交互极大的方便了电脑的使用。在iOS11中,你可以在iPhone或iPad上构建这种交互体验!
说在前面的话:
拖拽操作在iPad上是支持跨应用程序的,你可以从一个应用中拖取项目,通过Home键回到主界面并且打开另一个应用程序,然后将被拖拽的项目传递给这个应用程序中。在iPhone上,拖拽操作只支持当前应用程序内,你可以将某个元素从一个界面拖拽到另一个,这种维度的操作可以给设计人员更大的灵活性。
拖拽操作被设计成系统管理,开发者不需要为App申请特殊的用户权限。
二、拖拽源
对于拖拽操作,至少要有两个组件,一个组件作为拖拽源用来提供数据,一个组件作为拖拽目的用来接收数据,当前,同一个组件既可以是拖拽源也可以是拖拽目的。首先我们先来看拖拽源,在UIKit框架中,iOS11默认实现了一些组件可以作为拖拽源, 例如UITextField、UITextView、UITableView和UICollectionView等。文本组件默认支持拖拽操作进行文本的传递,对于列表组件则默认支持元素的拖拽。例如,在UITextField选中的文案中进行拖拽,可以将文字拖拽出来,效果如下图:
任意的UIView组件都可以作为拖拽源,让其成为拖拽源其实也十分简单,只需要3步:
1.创建一个UIDragInteraction行为对象。
2.设置UIDragInteraction对象的代理并实现相应方法。
3.将UIDragInteraction对象添加到指定View上。
最简单的可拖拽组件的创建示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.dragView]; }
-(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。
三、关于UIDragInteraction类
所有可以接收拖拽行为的组件都必须通过这个类实现,这个类中属性意义列举如下:
1 2 3 4 5 6 7 8 9 10
| - (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协议
UIDragInteractionDelegate用来处理拖拽源的行为与数据。其中定义了一个必须实现的方法和许多可选实现的方法。解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
- (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction itemsForBeginningSession:(id<UIDragSession>)session;
- (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;
- (BOOL)dragInteraction:(UIDragInteraction *)interaction sessionAllowsMoveOperation:(id<UIDragSession>)session;
- (BOOL)dragInteraction:(UIDragInteraction *)interaction sessionIsRestrictedToDraggingApplication:(id<UIDragSession>)session;
- (BOOL)dragInteraction:(UIDragInteraction *)interaction prefersFullSizePreviewsForSession:(id<UIDragSession>)session;
- (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;
-(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组件用来显示拖拽的文案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| - (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组件中。
六、关于UIDropInteraction类
与UIDragInteraction类类似,这个类的作用是让组件有相应放置操作的能力。其中属性如下:
1 2 3 4 5 6
| - (instancetype)initWithDelegate:(id<UIDropInteractionDelegate>)delegate;
@property (nonatomic, nullable, readonly, weak) id<UIDropInteractionDelegate> delegate;
@property (nonatomic, assign) BOOL allowsSimultaneousDropSessions;
|
七、UIDropInteractionDelegate协议
UIDropInteractionDelegate协议中所定义的方法全部是可选实现的,其用来处理用户放置交互行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| - (BOOL)dropInteraction:(UIDropInteraction *)interaction canHandleSession:(id<UIDropSession>)session;
- (void)dropInteraction:(UIDropInteraction *)interaction sessionDidEnter:(id<UIDropSession>)session;
- (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id<UIDropSession>)session;
- (void)dropInteraction:(UIDropInteraction *)interaction sessionDidExit:(id<UIDropSession>)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类用来进行处理回执,属性方法解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
- (instancetype)initWithDropOperation:(UIDropOperation)operation;
@property (nonatomic, readonly) UIDropOperation operation;
@property (nonatomic, getter=isPrecise) BOOL precise;
@property (nonatomic) BOOL prefersFullSizePreview;
|
八、拖拽数据载体UIDragItem类
UIDragItem类用来承载要传递的数据。其通过NSItemProvider类来进行构建,传递的数据类型是有严格规定的,必须遵守一定的协议,系统的NSString,NSAttributeString,NSURL,UIColor和UIImage是默认支持的,你可以直接传递这些数据。
UIDragItem中提供的属性方法:
1 2 3 4 5 6 7 8
| - (instancetype)initWithItemProvider:(NSItemProvider *)itemProvider;
@property (nonatomic, readonly) __kindof NSItemProvider *itemProvider;
@property (nonatomic, strong, nullable) id localObject;
@property (nonatomic, copy, nullable) UIDragPreview * _Nullable (^previewProvider)(void);
|
九、UIDropSession与UIDragSession
在与拖拽交互相关的接口中,这两个是面向协议编程的绝佳范例,首先在UIKit框架中只定义了这两个协议,而并没有相关的实现类,在拖拽行为的相关回调接口中,很多id类型的参数都遵守了这个协议,我们无需知道是哪个类实现的,直接进行使用即可:
UIDropSession:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @protocol UIDropSession <UIDragDropSession, NSProgressReporting>
@property (nonatomic, readonly, nullable) id<UIDragSession> localDragSession;
@property (nonatomic) UIDropSessionProgressIndicatorStyle progressIndicatorStyle;
- (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id<NSItemProviderReading>> *objects))completion;
@end
|
UIDragSession:
1 2 3 4 5
| API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos) @protocol UIDragSession <UIDragDropSession>
@property (nonatomic, strong, nullable) id localContext;
@end
|
UIDragDropSession:
1 2 3 4 5 6 7 8 9 10 11 12
| @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
UITargetedDragPreview专门用来处理拖放交互过程中的动画与预览视图。方法解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
- (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主要用来设置动画的起始视图与结束时回归的视图,其中属性方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
- (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用来进行拖拽动画的配置,解析如下:
1 2 3 4 5 6
| - (instancetype)initWithTextLineRects:(NSArray<NSValue *> *)textLineRects;
@property (nonatomic, copy, nullable) UIBezierPath *visiblePath;
@property (nonatomic, copy, null_resettable) UIColor *backgroundColor;
|
我们可以使用任意自定义的视图来展现这个预览动画,如下图所示:
十一、使用拖拽操作进行自定义数据的传递
本篇文章到这里,其实基本的内容都已经说完了,虽然比较详细,也可能难免冗余,如果你耐着性子看到了这里,那么我首先钦佩你的毅力并且感谢你的耐心。其实,拖拽交互如果进行只能对系统的提供的数据类型进行操作则应用就局限太多。试想一下,如果我们可以通过拖拽商品来进行购买,拖拽联系人来进行发送,或者在游戏中,拖拽进行卡片的融合,装备的提炼等等这种交互操作是不是会很畅快。最后,我们就来看看如何让自定义的数据类型支持拖拽操作。
首先你需要关注两个协议,NSItemProviderWriting与NSItemProviderReading。Writing协议用来让数据支持提供给数据源,Reading协议让数据支持从数据源读出,用自定义的Person类为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #import <Foundation/Foundation.h>
@interface Person : NSObject<NSItemProviderWriting,NSItemProviderReading>
@property(nonatomic,strong)NSString * name;
@property(nonatomic,assign)NSUInteger age;
@end
@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
|
需要注意,在拖放行为读取数据时的类型要对应,如下:
1 2 3 4 5 6
| -(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; }]; }
|
写了这么多,难免有疏漏与错误,欢迎指导交流