iPhone Duo 适配 Drag and Drop 实现方案

说明: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 里的数据表示。

核心理解:

  • UIDragInteractionUIDropInteraction 只负责手势和生命周期。
  • 真正跨区域、跨 App 传递数据的是 NSItemProvider
  • UTType 是双方约定的"数据协议"。
  • 读取数据是异步的,最终 UI 更新必须回主线程。
  • Duo 适配重点是左右 Pane 的布局、跨 Pane 数据恢复、以及不要只依赖 localObject

2. Duo 场景下的交互设计

典型结构可以是:

sql 复制代码
+-------------------+-------------------+
| Source Pane       | Drop Pane         |
|                   |                   |
| 可拖图片/文件/卡片 | 接收区/编辑区      |
+-------------------+-------------------+

实现原则:

  1. 左右区域不要写死屏幕宽度,用 Auto Layout / Size Class / Safe Area。
  2. Drop 判断基于 UTType,不要只判断某个具体类。
  3. 同 App 内拖拽可用 localObject 加速,但必须保留 NSItemProvider 读取兜底。
  4. 如果内部有 UIScrollView / UICollectionView,优先使用系统 drag/drop delegate,减少手势冲突。
  5. Drop 区域要给明确 hover 状态,比如边框高亮、背景变色。
  6. 大图不要同步解码,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 重排

参考:

相关推荐
也不知秋1 小时前
从“能回答”到“能干活”:AI Agent真正落地,需要哪些工程能力?
人工智能·程序员
可乐鸡翅yeah_2 小时前
HLS CORS 跨域问题完整排查实战,解决 M3U8 与 TS 分片跨域报错
android·ios·音视频·m3u8·音视频在线播放
黑科技iOS上架3 小时前
iOS预审工具相似度检测篇
ios·审核
光影少年4 小时前
React18 对RN 的影响
android·前端·react.js·ios·前端框架
得物技术4 小时前
企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术
java·程序员·架构
ZJPRENO5 小时前
2026 苹果秋季新品发布会总结
ios
ii_best5 小时前
iOS按键精灵大更新!引擎2.9.1+APP 2.5.3双发,支持18.7.1系统,蓝白巨魔都能装APP!
ios·自动化
杨晓龙4365 小时前
OpenMAIC 运行踩坑与经验
程序员
for_ever_love__6 小时前
iOS: GCD进阶,全局队列与自定义队列
macos·ios·objective-c·cocoa·多线程·gcd