说明:Apple Developer 当前已经把 iPhone Duo 作为适配主题之一展示,并列出多显示、多 Scene、相机体验等相关视频入口。Drag and Drop 在 Duo 这类双区域 / 展开态设备上,重点不是换 API,而是把"拖拽数据传递"和"左右区域布局"做对。
1. Drag and Drop 的本质
iOS Drag and Drop 分两层:
交互层
UIDragInteractionDelegate:负责"从哪里拖、拖什么、拖拽预览"。UIDropInteractionDelegate:负责"哪里能接、接收什么、drop 后怎么处理"。UICollectionViewDragDelegate/UICollectionViewDropDelegate:列表、宫格优先用这个。UITableViewDragDelegate/UITableViewDropDelegate:表格优先用这个。
数据层
NSItemProvider:拖拽数据的容器,本质是一个异步数据提供者。NSItemProviderWriting:自定义对象拖出去时实现。NSItemProviderReading:自定义对象拖进来时实现。UTType:声明数据类型,比如图片、PNG、文本、URL、PDF、视频。
Duo 适配时尤其要注意:不要依赖 localObject 作为唯一数据源。左右屏、不同 Scene、跨 App 时,真正可靠的是 NSItemProvider 里的数据表示。
核心理解:
UIDragInteraction和UIDropInteraction只负责手势和生命周期。- 真正跨区域、跨 App 传递数据的是
NSItemProvider。 UTType是双方约定的"数据协议"。- 读取数据是异步的,最终 UI 更新必须回主线程。
- Duo 适配重点是左右 Pane 的布局、跨 Pane 数据恢复、以及不要只依赖
localObject。
2. Duo 场景下的交互设计
典型结构可以是:
sql
+-------------------+-------------------+
| Source Pane | Drop Pane |
| | |
| 可拖图片/文件/卡片 | 接收区/编辑区 |
+-------------------+-------------------+
实现原则:
- 左右区域不要写死屏幕宽度,用 Auto Layout / Size Class / Safe Area。
- Drop 判断基于
UTType,不要只判断某个具体类。 - 同 App 内拖拽可用
localObject加速,但必须保留NSItemProvider读取兜底。 - 如果内部有
UIScrollView/UICollectionView,优先使用系统 drag/drop delegate,减少手势冲突。 - Drop 区域要给明确 hover 状态,比如边框高亮、背景变色。
- 大图不要同步解码,
loadObjectOfClass:回调后再回主线程更新 UI。
3. Objective-C 基础代码
拖拽源:
ini
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
- (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction
itemsForBeginningSession:(id<UIDragSession>)session {
UIImage *image = self.sourceImageView.image;
NSData *pngData = UIImagePNGRepresentation(image);
if (!pngData) {
return @[];
}
NSItemProvider *provider = [[NSItemProvider alloc] init];
[provider registerDataRepresentationForTypeIdentifier:UTTypePNG.identifier
visibility:NSItemProviderRepresentationVisibilityAll
loadHandler:^NSProgress *(void (^completionHandler)(NSData *, NSError *)) {
completionHandler(pngData, nil);
NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
progress.completedUnitCount = 1;
return progress;
}];
UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:provider];
item.localObject = image; // 仅同 App 内优化,不能只依赖它
return @[item];
}
Drop 接收:
objectivec
- (BOOL)dropInteraction:(UIDropInteraction *)interaction
canHandleSession:(id<UIDropSession>)session {
return [session hasItemsConformingToTypeIdentifiers:@[UTTypeImage.identifier]];
}
- (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction
sessionDidUpdate:(id<UIDropSession>)session {
UIDropOperation op = [self dropInteraction:interaction canHandleSession:session]
? UIDropOperationCopy
: UIDropOperationForbidden;
return [[UIDropProposal alloc] initWithDropOperation:op];
}
- (void)dropInteraction:(UIDropInteraction *)interaction
performDrop:(id<UIDropSession>)session {
UIDragItem *item = session.items.firstObject;
if ([item.localObject isKindOfClass:UIImage.class]) {
self.imageView.image = (UIImage *)item.localObject;
return;
}
[item.itemProvider loadObjectOfClass:UIImage.class
completionHandler:^(__kindof id<NSItemProviderReading> object, NSError *error) {
UIImage *image = [object isKindOfClass:UIImage.class] ? object : nil;
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}];
}
初始化交互:
ini
UIDragInteraction *drag = [[UIDragInteraction alloc] initWithDelegate:self];
drag.enabled = YES;
[self.sourceImageView addInteraction:drag];
UIDropInteraction *drop = [[UIDropInteraction alloc] initWithDelegate:self];
[self.dropContainerView addInteraction:drop];
4. Duo 适配注意事项
- 坐标转换 :不要用全局屏幕坐标判断落点,用
locationInView:。 - 跨 Pane 数据传递 :按跨进程思路实现,
NSItemProvider必须可完整恢复数据。 - 布局变化:折叠、展开、旋转时,source/drop pane 都要重新布局。
- 手势冲突:列表、宫格用系统 delegate;自定义 view 避免额外叠长按手势。
- 性能:大图片拖入后异步加载,主线程只做 UI 更新。
- 类型安全 :先
hasItemsConformingToTypeIdentifiers:,再读取。 - 外部 App 拖入 :不要假设一定是
UIImage,必要时用loadDataRepresentationForTypeIdentifier:兜底。
5. 推荐落地方式
在我们的 demo 里可以按"两栏模拟 Duo"实现:
- 左侧:图片来源区,挂
UIDragInteraction - 右侧:图片接收区,挂
UIDropInteraction - 数据类型:
UTTypeImage/UTTypePNG - 交互反馈:进入 drop 区高亮,离开恢复,drop 后渲染图片
- 后续扩展:支持文本、URL、文件、多图拖入、CollectionView 重排
参考: