关于 axiom-transferable-ref
为 Swift 开发者提供 CoreTransferable 和 SwiftUI 内容共享功能的参考技能,涵盖拖放、剪贴板和 ShareLink。还介绍自定义类型、UTType 声明及不同传输表示的选择。
主要功能
- 01实现拖放交互
- 02添加复制与粘贴支持
- 03通过 ShareLink 共享内容
- 04为自定义类型配置传输表示
- 05声明自定义 UTType
适用场景
- 01让 SwiftUI 模型支持拖放
- 02允许视图接收拖入文件
- 03为自定义内容添加复制粘贴
- 04为共享内容配置预览
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-transferable-ref安装后在 Claude Code 中输入/axiom-transferable-ref确认已加载。
详细介绍
可传输内容与内容共享参考
CoreTransferable 框架和 SwiftUI 共享界面的综合指南:拖放、复制/粘贴和 ShareLink。
何时使用此技能
- 实现拖放(
.draggable、.dropDestination) - 添加复制/粘贴支持(
.copyable、.pasteDestination、PasteButton) - 通过
ShareLink共享内容 - 让自定义类型可传输
- 为特定应用格式声明自定义 UTType
- 将
Transferable类型与 UIKit 的NSItemProvider桥接 - 在
CodableRepresentation、DataRepresentation、FileRepresentation和ProxyRepresentation之间进行选择
示例提示词
“如何让我的模型在 SwiftUI 中可拖动?” “ShareLink 没有显示我的自定义预览” “如何在视图中接收拖放的文件?” “DataRepresentation 和 FileRepresentation 有什么区别?” “如何为我的自定义类型添加复制/粘贴支持?” “我的拖放功能在应用内可用,但无法跨应用使用” “如何声明自定义 UTType?”
第 1 部分:快速参考
决策树:选择哪种 TransferRepresentation?
Your model type...
├─ Conforms to Codable + no specific binary format needed?
│ → CodableRepresentation
├─ Has custom binary format (Data in memory)?
│ → DataRepresentation (exporting/importing closures)
├─ Lives on disk (large files, videos, documents)?
│ → FileRepresentation (passes file URLs, not bytes)
├─ Need a fallback for receivers that don't understand your type?
│ → Add ProxyRepresentation (e.g., export as String or URL)
└─ Need to conditionally hide a representation?
→ Apply .exportingCondition to any representation
常见错误
| 错误 / 症状 | 原因 | 修复方法 |
|---|---|---|
| "Type does not conform to Transferable" | 缺少 transferRepresentation | 添加 static var transferRepresentation: some TransferRepresentation |
| 应用内可以拖放,但跨应用不行 | 未在 Info.plist 中声明自定义 UTType | 添加 UTExportedTypeDeclarations 条目 |
| 接收方始终收到纯文本,而非富类型 | ProxyRepresentation 排在 CodableRepresentation 之前 | 调整顺序:将最丰富的表示放在最前 |
| FileRepresentation 因 "file not found" 崩溃 | 接收方未在沙盒扩展过期前复制文件 | 在导入闭包中复制到应用存储空间 |
| PasteButton 始终处于禁用状态 | 粘贴板不包含匹配的 Transferable 类型 | 检查 UTType 一致性;确认粘贴的数据匹配 |
| ShareLink 显示通用预览 | 未提供 SharePreview,或图像不是 Transferable | 提供包含标题和图像的显式 SharePreview |
.dropDestination 闭包从未触发 | 载荷类型错误,或视图的命中测试区域为零 | 确认 for: 类型与拖动内容匹配;添加 .frame() 或 .contentShape() |
内置 Transferable 类型
这些类型无需额外代码即可使用——不需要一致性声明:
String、Data、URL、AttributedString、Image、Color
第 2 部分:让类型可传输
Transferable 协议只有一个要求:静态的 transferRepresentation 属性。
CodableRepresentation
最适合:已遵循 Codable 的模型。默认使用 JSON。
import UniformTypeIdentifiers
extension UTType {
static var todo: UTType = UTType(exportedAs: "com.example.todo")
}
struct Todo: Codable, Transferable {
var text: String
var isDone: Bool
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .todo)
}
}
自定义编码器/解码器(例如,使用 PropertyList 而非 JSON):
CodableRepresentation(
contentType: .todo,
encoder: PropertyListEncoder(),
decoder: PropertyListDecoder()
)
要求:自定义 UTType 需要在 Info.plist 中有匹配的 UTExportedTypeDeclarations(参见第 4 部分)。
DataRepresentation
最适合:数据位于内存中且由你控制序列化方式的自定义二进制格式。
struct ProfilesArchive: Transferable {
var profiles: [Profile]
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.toCSV()
} importing: { data in
try ProfilesArchive(csvData: data)
}
}
}
仅导入或仅导出的变体:
// Import only
DataRepresentation(importedContentType: .png) { data in
try MyImage(pngData: data)
}
// Export only
DataRepresentation(exportedContentType: .png) { image in
try image.pngData()
}
避免使用 UTType.data 作为内容类型——请使用 .png、.pdf、.commaSeparatedText 等特定类型。
FileRepresentation
适用于:磁盘上的大型负载(视频、文档、归档文件)。传递文件 URL,而不是将字节加载到内存中。
struct Video: Transferable {
let file: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .mpeg4Movie) { video in
SentTransferredFile(video.file)
} importing: { received in
// MUST copy — sandbox extension is temporary
let dest = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
try FileManager.default.copyItem(at: received.file, to: dest)
return Video(file: dest)
}
}
}
重要:received.file URL 带有临时沙盒扩展。在导入闭包中将文件复制到你自己的存储位置——闭包返回后,该 URL 将无法访问。
SentTransferredFile 属性:
file: URL— 文件位置allowAccessingOriginalFile: Bool— 为false(默认值)时,接收方会获得一个副本
ReceivedTransferredFile 属性:
file: URL— 磁盘上接收到的文件isOriginalFile: Bool— 这是发送方的原始文件还是副本
内容类型精度:.mpeg4Movie 仅匹配 .mp4 文件。要接受所有常见视频格式(.mp4、.mov、.m4v),请使用父类型 .movie,或为特定子类型声明多个 FileRepresentation:
// Broad: accept any video format the system recognizes
FileRepresentation(contentType: .movie) { ... } importing: { ... }
// Or specific: separate handlers per format
FileRepresentation(contentType: .mpeg4Movie) { ... } importing: { ... }
FileRepresentation(contentType: .quickTimeMovie) { ... } importing: { ... }
仅导入:如果你的类型只接收文件(作为放置目标,不导出文件),请使用仅导入初始化器——这能明确表达意图,并避免意外导出:
FileRepresentation(importedContentType: .movie) { received in
let dest = appStorageURL.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: dest)
return VideoClip(localURL: dest)
}
ProxyRepresentation
适用于:后备表示形式,让你的类型能够与预期接收更简单类型的接收方协作。
struct Profile: Transferable {
var name: String
var avatar: Image
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile)
ProxyRepresentation(exporting: \.name) // Fallback: paste as text
}
}
仅导出代理(常见模式——通常无法反向转换):
ProxyRepresentation(exporting: \.name) // Profile → String (one-way)
双向代理(适用于可以反向转换的情况):
ProxyRepresentation { item in
item.name // export
} importing: { name in
Profile(name: name) // import
}
组合多个表示形式
在 transferRepresentation 代码块中列出表示形式。顺序很重要——接收方会使用其支持的第一个表示形式。
struct Profile: Transferable {
static var transferRepresentation: some TransferRepresentation {
// 1. Richest: full profile data (apps that understand .profile)
CodableRepresentation(contentType: .profile)
// 2. Fallback: plain text (text fields, notes, any app)
ProxyRepresentation(exporting: \.name)
}
}
常见错误:将 ProxyRepresentation 放在首位,会导致同时支持两种表示形式的接收方始终获得降级版本。
条件导出
在运行时条件不满足时隐藏某个表示形式:
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.toCSV()
} importing: { data in
try Self(csvData: data)
}
.exportingCondition { archive in
archive.supportsCSV
}
可见性
控制哪些进程可以查看某个表示形式:
CodableRepresentation(contentType: .profile)
.visibility(.ownProcess) // Only within this app
选项:.all(默认)、.team(同一开发者团队)、.group(同一 App Group,macOS)、.ownProcess(仅同一应用)
建议文件名
为接收方写入磁盘时提供提示:
FileRepresentation(contentType: .mpeg4Movie) { video in
SentTransferredFile(video.file)
} importing: { received in
// ...
}
.suggestedFileName("My Video.mp4")
// Or dynamic:
.suggestedFileName { video in video.title + ".mp4" }
第 3 部分:SwiftUI 界面
ShareLink
标准的分享入口。接受任何 Transferable 类型。
// Simple: share a string
ShareLink(item: "Check out this app!")
// With preview
ShareLink(
item: photo,
preview: SharePreview(photo.caption, image: photo.image)
)
// Share a URL with custom preview (prevents system metadata fetch)
ShareLink(
item: URL(string: "https://example.com")!,
preview: SharePreview("My Site", image: Image("hero"))
)
分享多个项目并为每个项目提供预览:
ShareLink(items: photos) { photo in
SharePreview(photo.caption, image: photo.image)
}
SharePreview 初始化器:
SharePreview("Title")— 仅文本SharePreview("Title", image: someImage)— 文本 + 全尺寸图片SharePreview("Title", icon: someIcon)— 文本 + 缩略图图标SharePreview("Title", image: someImage, icon: someIcon)— 三者都有
注意:如果自定义类型未提供 SharePreview,分享面板会显示通用预览。对于重要的自定义类型,务必提供预览。
拖放
使视图可拖动:
Text(profile.name)
.draggable(profile)
使用自定义拖动预览:
Text(profile.name)
.draggable(profile) {
Label(profile.name, systemImage: "person")
.padding()
.background(.regularMaterial)
}
接受放置:
Color.clear
.frame(width: 200, height: 200)
.dropDestination(for: Profile.self) { profiles, location in
guard let profile = profiles.first else { return false }
self.droppedProfile = profile
return true
} isTargeted: { isTargeted in
self.isDropTargeted = isTargeted
}
多种项目类型——使用符合 Transferable 的枚举包装类型,而不是叠加 .dropDestination 修饰符(叠加可能导致只有最外层的处理器触发):
enum DroppableItem: Transferable {
case image(Image)
case text(String)
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { (image: Image) in DroppableItem.image(image) }
ProxyRepresentation { (text: String) in DroppableItem.text(text) }
}
}
myView
.dropDestination(for: DroppableItem.self) { items, _ in
for item in items {
switch item {
case .image(let img): handleImage(img)
case .text(let str): handleString(str)
}
}
return true
}
使用 ForEach 重新排序——与 .onMove 结合使用,或使用 draggable/dropDestination 在容器之间移动项目。
剪贴板(复制/粘贴)
复制支持(启用“编辑 > 复制”/ Cmd+C):
List(items) { item in
Text(item.name)
}
.copyable(items)
粘贴支持(启用“编辑 > 粘贴”/ Cmd+V):
List(items) { item in
Text(item.name)
}
.pasteDestination(for: Item.self) { pasted in
items.append(contentsOf: pasted)
} validator: { candidates in
candidates.filter { $0.isValid }
}
验证器闭包会在操作前运行——返回空数组即可阻止粘贴。
剪切支持:
.cuttable(for: Item.self) {
let selected = items.filter { $0.isSelected }
items.removeAll { $0.isSelected }
return selected
}
PasteButton——用于处理粘贴并按类型筛选的系统按钮:
PasteButton(payloadType: String.self) { strings in
notes.append(contentsOf: strings)
}
平台差异:PasteButton 会在 iOS 上自动验证粘贴板的更改,但在 macOS 上不会。
可用性:.copyable、.pasteDestination 和 .cuttable 仅适用于 macOS 13+——iOS 上不存在这些 API。在 iOS 上,粘贴请使用 PasteButton(iOS 16+);编程方式复制或剪切,请使用标准上下文菜单或 UIPasteboard。PasteButton 跨平台可用:macOS 10.15+、iOS 16+、visionOS 1.0+。
第 4 部分:UTType 声明
系统类型
尽可能使用 Apple 内置的 UTTypes——系统已能识别这些类型:
import UniformTypeIdentifiers
// Common types
UTType.plainText // public.plain-text
UTType.utf8PlainText // public.utf8-plain-text
UTType.json // public.json
UTType.png // public.png
UTType.jpeg // public.jpeg
UTType.pdf // com.adobe.pdf
UTType.mpeg4Movie // public.mpeg-4
UTType.commaSeparatedText // public.comma-separated-values-text
声明自定义类型
步骤 1:在 Swift 中声明:
extension UTType {
static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
}
步骤 2:在 Info.plist 的 UTExportedTypeDeclarations 下添加:
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.myapp.recipe</string>
<key>UTTypeDescription</key>
<string>Recipe</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>recipe</string>
</array>
</dict>
</dict>
</array>
两者缺一不可。 仅添加 Swift 声明虽能通过编译,但若未在 Info.plist 中添加对应条目,跨应用传输会静默失败。
导入类型与导出类型
- 导出(
exportedAs:)——此类型归你的应用所有。适用于应用专属格式。 - 导入(
importedAs:)——此类型归其他应用所有。适用于需要接收其他应用所用格式的情况。
UTType 遵循关系
自定义类型应遵循系统类型,以获得更广泛的兼容性:
// Your .recipe conforms to public.data (binary data)
// This means any receiver that accepts generic data can also accept recipes
常见的遵循父类型:public.data、public.content、public.text、public.image
第 5 部分:UIKit 桥接
NSItemProvider + Transferable
在 UIKit 的 NSItemProvider(用于 UIActivityViewController、扩展、拖动会话)与 Transferable 之间进行桥接:
// Load a Transferable from an NSItemProvider
let provider: NSItemProvider = // from drag session, extension, etc.
provider.loadTransferable(type: Profile.self) { result in
switch result {
case .success(let profile):
// Use the profile
case .failure(let error):
// Handle error
}
}
何时使用 UIActivityViewController
ShareLink 可满足大多数分享需求。以下情况请使用 UIActivityViewController:
- 需要自定义活动项或排除某些活动类型
- 需要使用
UIActivityItemsConfiguration延迟提供项目 - 需要自定义
UIActivity子类 - 需要以编程方式控制呈现
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {}
}
对于大多数应用,ShareLink 已足够且更值得优先使用——它原生集成了 Transferable。
第 6 部分:注意事项与故障排除
FileRepresentation 临时文件生命周期
FileRepresentation 导入闭包中的 received.file URL 带有临时沙盒扩展。闭包返回后,系统可能会撤销访问权限。务必复制文件:
// WRONG — file may become inaccessible
return Video(file: received.file)
// RIGHT — copy to your own storage
let dest = myAppDirectory.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: dest)
return Video(file: dest)
文件拖放后的异步工作
FileRepresentation 导入闭包是同步的——无法在其中使用 await。请先复制文件并返回模型,然后再对复制后的 URL 执行异步后处理(生成缩略图、转码、提取元数据):
// WRONG — can't await in the importing closure
FileRepresentation(importedContentType: .movie) { received in
let dest = ...
try FileManager.default.copyItem(at: received.file, to: dest)
let thumbnail = await generateThumbnail(for: dest) // ❌ compile error
return VideoClip(localURL: dest, thumbnail: thumbnail)
}
// RIGHT — return immediately, process async afterward
// In your view model or drop handler:
.dropDestination(for: VideoClip.self) { clips, _ in
for clip in clips {
timeline.append(clip)
Task {
// clip.localURL is the COPY — safe to access anytime
let thumbnail = await generateThumbnail(for: clip.localURL)
clip.thumbnail = thumbnail
}
}
return true
}
表示形式的顺序
系统会按声明顺序尝试表示形式。接收方会使用它支持的第一个表示形式。
// WRONG — receivers always get plain text
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation(exporting: \.name) // ← every receiver supports String
CodableRepresentation(contentType: .profile) // ← never reached
}
// RIGHT — richest first, fallbacks last
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile) // ← apps that understand Profile
ProxyRepresentation(exporting: \.name) // ← fallback for everyone else
}
自定义 UTType 未添加到 Info.plist
如果你在 Swift 中声明了 UTType(exportedAs: "com.myapp.type"),却忘记在 Info.plist 中添加对应条目:
- 应用内传输正常(同一进程可识别此类型)
- 跨应用传输会静默失败(其他应用无法解析此类型)
这是最常见的“开发时正常,生产环境失败”问题。
放置目标命中测试
.dropDestination 要求视图具有非零尺寸的 frame 才能进行命中测试。如果放置操作未被识别:
// WRONG — Color.clear has zero intrinsic size
Color.clear
.dropDestination(for: Image.self) { ... }
// RIGHT — give it a frame
Color.clear
.frame(width: 200, height: 200)
.contentShape(Rectangle()) // ensure full area is hit-testable
.dropDestination(for: Image.self) { ... }
使用 loadTransferable 异步加载
NSItemProvider.loadTransferable 是异步的。请在主 Actor 上更新 UI:
provider.loadTransferable(type: Profile.self) { result in
Task { @MainActor in
switch result {
case .success(let profile):
self.profile = profile
case .failure(let error):
self.errorMessage = error.localizedDescription
}
}
}
PasteButton 的平台差异
在 iOS 上,PasteButton 会根据粘贴板的变化自动验证——粘贴板内容变化时,按钮会自动启用或禁用。在 macOS 上,不会进行这种自动验证。如果你的 macOS 应用需要动态验证粘贴内容,请监视 UIPasteboard.changedNotification(UIKit),或手动检查 NSPasteboard 的 change count。
资源
WWDC:2022-10062、2022-10052、2022-10023、2022-10093、2022-10095
文档:/coretransferable/transferable、/coretransferable/choosing-a-transfer-representation-for-a-model-type、/coretransferable/filerepresentation、/coretransferable/proxyrepresentation、/swiftui/sharelink、/swiftui/drag-and-drop、/swiftui/clipboard、/uniformtypeidentifiers
Skills:axiom-photo-library、axiom-codable、axiom-swiftui-gestures、axiom-app-intents-ref