在 Mac 应用的两个面板之间复制合成图形时,建议从 SwiftUI 类型化拖放开始。如果现有提供器接口或较早版本的 macOS 部署目标要求使用项目提供器,再选择它们。在选择任一 API 之前,先定义负载、接收规则和模型所有者。
定义成功放置的含义
这里的读者任务很明确:让用户将合成圆形或方形从调色板拖到同一 Mac 应用中的一个集合里。目标位置会创建一个新项目;调色板保持不变。所有内容都保存在内存中,关闭窗口后集合就会丢弃。
Apple 建议 Swift 拖放使用 transferable 项目。其文档也支持基于提供器的修饰符,因此具体选择取决于部署要求和需要集成的接口。拖放 — Apple。
实现视图之前,先写下传输契约:
| 契约问题 | 本示例中的决定 |
|---|---|
| 跨边界传递什么? | 描述图形种类的带版本值 |
| 谁可以接收此表示? | 仅源应用 |
| 成功放置会做什么? | 向集合追加一个新标识的项目 |
| 谁负责执行变更? | 目标视图的集合状态 |
| 取消或拒绝时会怎样? | 模型不变 |
| 放置位置代表什么? | 在这里没有含义;已接受的项目按顺序追加 |
这些属于应用设计决策。拖动预览展示负载,而目标位置决定负载如何改变模型。将这两项职责分开,就能在不重写集合规则的情况下更换传输方式。
选择最能表达契约的精简 API
| 情况 | 起点 | 原因 |
|---|---|---|
| SwiftUI 源视图和目标视图,macOS 13 或更高版本 | Transferable、.draggable 和类型化的 .dropDestination |
直接表达可接收的模型类型 |
现有 NSItemProvider 集成 |
.onDrag 和 .onDrop |
保留显式的表示注册和加载 |
| 部署目标早于 macOS 13 | 检查该目标支持哪些提供器修饰符 | 类型化基线要求 macOS 13;下文讨论使用的提供器重载要求 macOS 11 |
| 现有 AppKit 视图负责鼠标交互 | AppKit 拖动 API | 保留视图的事件处理和拖动会话生命周期 |
Apple 说明了 macOS 13 起提供的 Transferable 和原始类型化放置目标。基于提供器的 onDrop(of:isTargeted:perform:) 重载接受 UTType 值数组,最低支持版本为 macOS 11。请检查具体重载,不要假设所有拖放 API 的最低版本都相同。Transferable、dropDestination(for:action:isTargeted:)、onDrop(of:isTargeted:perform:)。
两种方法都支持仅限本应用的可见性。对于类型化表示,应用 .visibility(.ownProcess);Apple 将该级别定义为仅在源应用内可见。因此,仅仅为了让传输留在应用内,无需选择项目提供器。visibility(_:)、ownProcess。
构建类型化复制交互
这个完整示例由新 macOS SwiftUI 应用目标中的两个 Swift 文件组成。将部署目标设为 macOS 13 或更高版本,并在此基线中使用 Swift 5 语言模式。用第二个文件替换生成的应用入口点;只保留一个 @main 声明。代码已经检查过,但此处无法编译,因为可用的 shell 没有 Swift 工具链。
声明负载及其内容类型
在目标的 Info 属性列表中添加导出的类型声明。在 Xcode 的属性列表编辑器中,将 UTExportedTypeDeclarations 创建为数组,并在其中放入一个字典。将该字典的 UTTypeIdentifier 设为 com.example.shape-transfer,UTTypeDescription 设为 Synthetic Shape Transfer,并将 UTTypeConformsTo 设为包含 public.data 的数组。这个标识符仅作示例;实际应用应使用唯一标识符。
Swift 声明和属性列表标识符必须一致。Apple 要求为自定义统一类型添加相应的属性列表条目,并说明了应用定义格式的导出声明。此示例传输的是数据而非文件,因此不添加文件扩展名。为应用定义文件和数据类型。
创建 ShapePayload.swift:
import Foundation
import CoreTransferable
import UniformTypeIdentifiers
extension UTType {
static let shapeTransfer = UTType(
exportedAs: "com.example.shape-transfer",
conformingTo: .data
)
}
enum ShapeKind: String, Codable, Sendable {
case circle = "Circle"
case square = "Square"
}
struct ShapePayload: Codable, Sendable, Transferable {
let version: Int
let kind: ShapeKind
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .shapeTransfer)
.visibility(.ownProcess)
}
}
struct PlacedShape: Identifiable {
let id = UUID()
let kind: ShapeKind
}
CodableRepresentation 负责在负载与可传输数据之间进行转换。负载包含一个值,而不是视图或可变集合引用。其显式 Sendable 遵循 Swift 规则:可发送结构体所存储的成员本身也必须可发送。Transferable、Sendable。
这里有意只使用一种表示。文本代理会增加另一种使用项目的方式,但对这个契约没有帮助。Apple 介绍了表示顺序和回退格式;只有接收方确实需要时才添加它们。将视图变为拖动源。
接收负载并提交一次集合更新
创建 ShapeTransferApp.swift:
import SwiftUI
@main
struct ShapeTransferApp: App {
var body: some Scene {
WindowGroup {
ShapeTransferView()
}
}
}
@MainActor
struct ShapeTransferView: View {
@State private var placed: [PlacedShape] = []
@State private var targeted = false
@State private var message = "Drag a shape into Collection."
var body: some View {
HStack(alignment: .top, spacing: 24) {
VStack(spacing: 16) {
Text("Palette").font(.headline)
paletteItem(.circle)
paletteItem(.square)
}
.frame(width: 160)
VStack(alignment: .leading, spacing: 12) {
Text("Collection").font(.headline)
Text(message)
ForEach(placed) { item in
Label(item.kind.rawValue,
systemImage: symbol(for: item.kind))
}
Spacer(minLength: 20)
}
.padding()
.frame(width: 300, height: 320, alignment: .topLeading)
.background(targeted ? Color.blue.opacity(0.15)
: Color.gray.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(
for: ShapePayload.self,
action: { payloads, _ in
guard !payloads.isEmpty,
payloads.allSatisfy({ $0.version == 1 })
else {
message = "Drop rejected: unsupported payload."
return false
}
let additions = payloads.map {
PlacedShape(kind: $0.kind)
}
placed.append(contentsOf: additions)
message = "Added \(additions.count) shape(s)."
return true
},
isTargeted: { targeted = $0 }
)
}
.padding(24)
}
private func paletteItem(_ kind: ShapeKind) -> some View {
Label(kind.rawValue, systemImage: symbol(for: kind))
.padding()
.background(Color.gray.opacity(0.12))
.draggable(ShapePayload(version: 1, kind: kind))
}
private func symbol(for kind: ShapeKind) -> String {
switch kind {
case .circle: return "circle.fill"
case .square: return "square.fill"
}
}
}
显式的 action: 和 isTargeted: 标签会选择原始类型化目标重载。Apple 将其布尔返回值定义为成功或失败,并将位置参数定义为目标视图坐标空间中的位置。这里有意不使用该位置,因为集合只会追加图形,不会按位置放置。dropDestination(for:action:isTargeted:)。
接收规则会先检查收到的所有负载,再追加任何内容。不支持的版本会导致整批已接收内容被拒绝;每个有效负载都会获得一个新的目标标识。因此,再次从调色板拖动同一项目会创建另一个副本。此规则针对传递给闭包的值;解码失败发生在这些值成为 ShapePayload 之前。
目标状态只会改变呈现效果,不会插入项目,也不会从源位置移除任何内容。这种职责划分也让取消操作有了明确结果:没有被接受的放置时,集合保持不变。
集成要求使用项目提供器时再采用它们
提供器实现可以传递相同的版本和图形种类。保持契约不变,并按以下步骤替换传输方式:
- 在
.onDrag闭包中创建NSItemProvider,并使用.ownProcess可见性,在同一个自定义类型标识符下注册数据表示。 - 在加载处理程序中捕获不可变负载值或准备好的字节。对该值进行编码,并通过表示完成处理程序返回数据或错误。
- 配置
.onDrop以接收自定义类型。 - 在放置操作中,立即使用
loadDataRepresentation(forTypeIdentifier:completionHandler:)开始加载每个已接收的提供器。 - 解码返回的数据,验证版本并收集结果。只有在所需结果全部可用后,才在主 actor 上执行最终集合变更。
Apple 说明了提供器的显式数据注册和仅限本应用的可见性。其加载方法是异步的,并会返回一个进度对象。类型标识符匹配只表示可以尝试加载;并不保证加载和解码一定成功。registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:)、NSItemProviderRepresentationVisibility.ownProcess、loadDataRepresentation(forTypeIdentifier:completionHandler:)。
有两个时间边界需要注意。首先,Apple 指出,提供器加载必须在 .onDrop 操作期间开始,但可以稍后完成。在其他 actor 上通过稍后的任务启动首次加载调用,不符合这一文档要求。其次,Apple 警告不要在表示处理程序中将导出工作派发到主 actor 或主线程,因为这可能导致挂起。导出工作应独立于 UI 变更;导入完成后再更新集合状态。onDrop(of:isTargeted:perform:)、onDrag(_:)。
对于异步提供器实现,应区分接受加载任务和完成模型事务。使用带索引的结果保留提供器顺序,并报告加载或解码失败;如果契约要求所有项目都成功,则任何失败都应使集合保持不变。不要通过过滤操作悄悄丢弃失败项目。这些实现规则源于示例的批次接收策略。
保留 AppKit 事件所有权和移动语义
如果现有 AppKit 视图已经负责鼠标交互,就让它继续在其中启动会话。Apple 的 beginDraggingSession(with:event:source:) 接收发起拖动的鼠标按下事件、坐标采用该视图坐标系的拖动项目,以及一个符合 NSDraggingSource 的源对象。源回调涵盖会话开始、移动以及完成或取消。beginDraggingSession(with:event:source:)。
AppKit 表示边界也很明确:NSDraggingItem(pasteboardWriter:) 要求提供一个 NSPasteboardWriting 对象。SwiftUI 提供器回调和 AppKit 剪贴板写入对象是不同的接口。将自定义 AppKit 视图桥接到 SwiftUI 时,应由 AppKit 视图继续负责其事件和会话;由共享模型负责已接收的集合变更。init(pasteboardWriter:)。
此示例执行的是复制。如果任务改为在集合之间移动现有项目,就添加稳定的项目标识符,并明确指定唯一的移除责任方。共享存储可以只迁移一次项目;或者目标位置先插入项目,再由源位置在确认移动完成后移除。混用这两种策略可能导致项目被移除两次。
Apple 当前文档包含 macOS 26 的 DragConfiguration、基于会话的类型化放置重载以及拖动会话观察功能。源位置提出支持的操作,目标位置选择操作,而会话完成回调让源位置能够响应结果。采用此路径前,请检查各个符号的 SDK 可用性。DragConfiguration、dropDestination(for:isEnabled:action:)、onDragSessionUpdated(_:)。
不要在指针进入目标位置或预览出现时移除源项目。这些事件都早于接收操作。对于应用内移动,一个实用的不变量是:事务完成后,每个逻辑项目只属于一个集合。
通过可观察结果验证契约
在 Mac 上按以下顺序检查实现:
- 将
Circle拖入Collection:目标集合增加一个圆形,调色板仍保留原项目。 - 重复拖动:目标集合增加第二个具有独立标识的项目。
- 将
Square拖到目标区域之外并释放:集合不变。 - 暂时将源负载版本改为
2:目标报告拒绝且不添加任何内容;之后恢复为1。 - 对于提供器实现,故意返回编码或加载错误:显示失败,并且不插入部分批次。
- 对于移动实现,取消拖动并确认原项目仍属于原来的集合。
对于这个双面板复制任务,应从类型化 SwiftUI 开始,除非有具体的兼容性或集成要求需要使用提供器或 AppKit。需要长期保持的是这份契约:明确的值跨越边界,目标位置验证该值,然后由一个模型所有者提交已接受的变更。
来源
拖放 — Apple。
Transferable — Apple。
将视图变为拖动源 — Apple。
visibility(_:) — Apple。
ownProcess — Apple。
为应用定义文件和数据类型 — Apple。
Sendable — Apple。
onDrag(_:) — Apple。
onDrop(of:isTargeted:perform:) — Apple。
registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:) — Apple。
loadDataRepresentation(forTypeIdentifier:completionHandler:) — Apple。
init(pasteboardWriter:) — Apple。
DragConfiguration — Apple。
onDragSessionUpdated(_:) — Apple。