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

核心理解:

  • UIDragInteraction 和 UIDropInteraction 只负责手势和生命周期。
  • 真正跨区域、跨 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 重排

参考:

相关推荐
美狐美颜SDK开放平台8 小时前
准备开发直播APP?视频美颜SDK接入前的十项检查清单
android·ios·音视频·直播美颜sdk·视频美颜sdk
青衫嵌入式11 小时前
用 STM32 搞清楚 FreeRTOS 的任务上下文到底是什么
物联网·程序员
Solara19 小时前
进推荐流的文章不是快 1.42 倍,是 3.36 倍:我把错的数写进了长期记忆
人工智能·程序员·ai编程
承渊政道20 小时前
iOS远程控制电脑用什么好?iPhoneiPad远程控制Windows电脑最佳方案2026横评
windows·ios·远程控制·1024程序员节·iphone ipad
茶底世界之下20 小时前
彩色建筑适合转黑白吗?用 Awaking 检查颜色消失后的层次
ios
2601_9679791220 小时前
当代码有了温度:程序员的键盘上,藏着人间烟火
程序员·生活·技术·代码·温情
黑科技iOS上架21 小时前
App Store 26年10月新应用审核放缓大量应用不进审
ios·审核
OxYGC1 天前
手机为什么能“自动跳过“开屏广告?李跳跳、GKD 与 iOS 代理软件原理盘点
android·ios·开屏广告·广告屏蔽
leobertlan1 天前
杂篇 -- 拂拭念头、造器与自洽:AI 时代属于每个人的秩序
人工智能·程序员
终端安全笔记1 天前
一份配置管十项 AI 功能:它关的是入口,不是数据
网络安全·ios·移动设备管理