mcpField
A
axiom-transferable-ref

axiom-transferable-ref

github.com/charleswiltgen开发1.2k100 次安装今天更新
代码仓库

关于 axiom-transferable-ref

为 Swift 开发者提供 CoreTransferable 和 SwiftUI 内容共享功能的参考技能,涵盖拖放、剪贴板和 ShareLink。还介绍自定义类型、UTType 声明及不同传输表示的选择。

主要功能

  1. 01实现拖放交互
  2. 02添加复制与粘贴支持
  3. 03通过 ShareLink 共享内容
  4. 04为自定义类型配置传输表示
  5. 05声明自定义 UTType

适用场景

  1. 01让 SwiftUI 模型支持拖放
  2. 02允许视图接收拖入文件
  3. 03为自定义内容添加复制粘贴
  4. 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

评论

支持 Markdown