axiom-transferable-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Transferable & Content Sharing Reference

Transferable 与内容分享参考指南

Comprehensive guide to the CoreTransferable framework and SwiftUI sharing surfaces: drag and drop, copy/paste, and ShareLink.
本文是关于CoreTransferable框架和SwiftUI分享交互组件的综合指南:包括拖拽、复制粘贴和ShareLink。

When to Use This Skill

何时使用本技能

  • Implementing drag and drop (
    .draggable
    ,
    .dropDestination
    )
  • Adding copy/paste support (
    .copyable
    ,
    .pasteDestination
    ,
    PasteButton
    )
  • Sharing content via
    ShareLink
  • Making custom types transferable
  • Declaring custom UTTypes for app-specific formats
  • Bridging
    Transferable
    types with UIKit's
    NSItemProvider
  • Choosing between
    CodableRepresentation
    ,
    DataRepresentation
    ,
    FileRepresentation
    , and
    ProxyRepresentation
  • 实现拖拽功能(
    .draggable
    .dropDestination
  • 添加复制粘贴支持(
    .copyable
    .pasteDestination
    PasteButton
  • 通过
    ShareLink
    分享内容
  • 让自定义类型支持Transferable
  • 为应用专属格式声明自定义UTType
  • 将Transferable类型与UIKit的
    NSItemProvider
    进行桥接
  • CodableRepresentation
    DataRepresentation
    FileRepresentation
    ProxyRepresentation
    之间做选择

Example Prompts

示例提问

"How do I make my model draggable in SwiftUI?" "ShareLink isn't showing my custom preview" "How do I accept dropped files in my view?" "What's the difference between DataRepresentation and FileRepresentation?" "How do I add copy/paste support for my custom type?" "My drag and drop works within the app but not across apps" "How do I declare a custom UTType?"

"如何让我的模型在SwiftUI中支持拖拽?" "ShareLink没有显示我的自定义预览" "如何在我的视图中接收拖入的文件?" "DataRepresentation和FileRepresentation有什么区别?" "如何为我的自定义类型添加复制粘贴支持?" "我的拖拽功能在应用内有效,但跨应用无效" "如何声明自定义UTType?"

Part 1: Quick Reference

第一部分:快速参考

Decision Tree: Which TransferRepresentation?

决策树:选择哪种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
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

Common Errors

常见错误

Error / SymptomCauseFix
"Type does not conform to Transferable"Missing
transferRepresentation
Add
static var transferRepresentation: some TransferRepresentation
Drop works in-app but not across appsCustom UTType not declared in Info.plistAdd
UTExportedTypeDeclarations
entry
Receiver always gets plain text instead of rich typeProxyRepresentation listed before CodableRepresentationReorder: richest representation first
FileRepresentation crashes with "file not found"Receiver didn't copy file before sandbox extension expiredCopy to app storage in the importing closure
PasteButton always disabledPasteboard doesn't contain matching Transferable typeCheck UTType conformance; verify the pasted data matches
ShareLink shows generic previewNo
SharePreview
provided or image isn't
Transferable
Supply explicit
SharePreview
with title and image
.dropDestination
closure never fires
Wrong payload type or view has zero hit-test areaVerify
for:
type matches dragged content; add
.frame()
or
.contentShape()
错误/症状原因解决方法
"Type does not conform to Transferable"缺少
transferRepresentation
添加
static var transferRepresentation: some TransferRepresentation
拖拽在应用内有效,但跨应用无效自定义UTType未在Info.plist中声明添加
UTExportedTypeDeclarations
条目
接收方总是获取纯文本而非富类型ProxyRepresentation在CodableRepresentation之前声明调整顺序:先声明最丰富的表示类型
FileRepresentation崩溃,提示"file not found"接收方在沙箱扩展过期前未复制文件在导入闭包中将文件复制到应用存储目录
PasteButton始终处于禁用状态剪贴板中没有匹配的Transferable类型检查UTType一致性;验证粘贴的数据是否匹配
ShareLink显示通用预览未提供
SharePreview
或图片不支持Transferable
提供包含标题和图片的显式
SharePreview
.dropDestination
闭包从未触发
负载类型错误或视图点击测试区域为零验证
for:
类型与拖拽内容匹配;添加
.frame()
.contentShape()

Built-in Transferable Types

内置Transferable类型

These work with zero additional code — no conformance needed:
String
,
Data
,
URL
,
AttributedString
,
Image
,
Color

这些类型无需额外代码即可使用——无需实现协议:
String
,
Data
,
URL
,
AttributedString
,
Image
,
Color

Part 2: Making Types Transferable

第二部分:让类型支持Transferable

The
Transferable
protocol has one requirement: a static
transferRepresentation
property.
Transferable
协议只有一个要求:静态的
transferRepresentation
属性。

CodableRepresentation

CodableRepresentation

Best for: models already conforming to
Codable
. Uses JSON by default.
swift
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)
    }
}
Custom encoder/decoder (e.g., PropertyList instead of JSON):
swift
CodableRepresentation(
    contentType: .todo,
    encoder: PropertyListEncoder(),
    decoder: PropertyListDecoder()
)
Requirement: Custom UTTypes need matching
UTExportedTypeDeclarations
in Info.plist (see Part 4).
最佳适用场景:已遵循
Codable
的模型。默认使用JSON格式。
swift
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):
swift
CodableRepresentation(
    contentType: .todo,
    encoder: PropertyListEncoder(),
    decoder: PropertyListDecoder()
)
注意:自定义UTType需要在Info.plist中添加对应的
UTExportedTypeDeclarations
(见第四部分)。

DataRepresentation

DataRepresentation

Best for: custom binary formats where data is in memory and you control serialization.
swift
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 or export-only variants:
swift
// Import only
DataRepresentation(importedContentType: .png) { data in
    try MyImage(pngData: data)
}

// Export only
DataRepresentation(exportedContentType: .png) { image in
    try image.pngData()
}
Avoid using
UTType.data
as the content type — use a specific type like
.png
,
.pdf
,
.commaSeparatedText
.
最佳适用场景:数据在内存中且你控制序列化逻辑的自定义二进制格式。
swift
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)
        }
    }
}
仅导入或仅导出的变体:
swift
// 仅导入
DataRepresentation(importedContentType: .png) { data in
    try MyImage(pngData: data)
}

// 仅导出
DataRepresentation(exportedContentType: .png) { image in
    try image.pngData()
}
避免使用
UTType.data
作为内容类型——使用特定类型如
.png
.pdf
.commaSeparatedText

FileRepresentation

FileRepresentation

Best for: large payloads on disk (videos, documents, archives). Passes file URLs instead of loading bytes into memory.
swift
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)
        }
    }
}
Critical: The
received.file
URL has a temporary sandbox extension. Copy the file to your own storage in the importing closure — the URL becomes inaccessible after the closure returns.
SentTransferredFile
properties:
  • file: URL
    — the file location
  • allowAccessingOriginalFile: Bool
    — when
    false
    (default), receiver gets a copy
ReceivedTransferredFile
properties:
  • file: URL
    — the received file on disk
  • isOriginalFile: Bool
    — whether this is the sender's original file or a copy
Content type precision:
.mpeg4Movie
only matches
.mp4
files. To accept all common video formats (
.mp4
,
.mov
,
.m4v
), use the parent type
.movie
— or declare multiple
FileRepresentation
s for specific subtypes:
swift
// 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: { ... }
Import-only: When your type only receives files (drop target, no export), use the import-only initializer — it makes intent explicit and avoids accidental export:
swift
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)
}
最佳适用场景:磁盘上的大负载(视频、文档、归档文件)。传递文件URL而非字节数据。
swift
struct Video: Transferable {
    let file: URL

    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .mpeg4Movie) { video in
            SentTransferredFile(video.file)
        } importing: { received in
            // 必须复制——沙箱扩展是临时的
            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拥有临时沙箱扩展。系统可能在闭包返回后撤销访问权限。务必在导入闭包中将文件复制到你自己的存储目录。
SentTransferredFile
属性:
  • file: URL
    — 文件路径
  • allowAccessingOriginalFile: Bool
    — 当为
    false
    (默认)时,接收方会获取文件副本
ReceivedTransferredFile
属性:
  • file: URL
    — 磁盘上的接收文件
  • isOriginalFile: Bool
    — 这是发送方的原始文件还是副本
内容类型精度
.mpeg4Movie
仅匹配
.mp4
文件。要接受所有常见视频格式(
.mp4
.mov
.m4v
),请使用父类型
.movie
——或者为特定子类型声明多个
FileRepresentation
swift
// 宽泛:接受系统识别的任何视频格式
FileRepresentation(contentType: .movie) { ... } importing: { ... }

// 或特定:为每种格式单独处理
FileRepresentation(contentType: .mpeg4Movie) { ... } importing: { ... }
FileRepresentation(contentType: .quickTimeMovie) { ... } importing: { ... }
仅导入:当你的类型仅接收文件(作为拖放目标,不导出)时,使用仅导入的初始化器——它能明确表达意图并避免意外导出:
swift
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

ProxyRepresentation

Best for: fallback representations that let your type work with receivers expecting simpler types.
swift
struct Profile: Transferable {
    var name: String
    var avatar: Image

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .profile)
        ProxyRepresentation(exporting: \.name)  // Fallback: paste as text
    }
}
Export-only proxy (common pattern — reverse conversion often impossible):
swift
ProxyRepresentation(exporting: \.name)  // Profile → String (one-way)
Bidirectional proxy (when reverse makes sense):
swift
ProxyRepresentation { item in
    item.name  // export
} importing: { name in
    Profile(name: name)  // import
}
最佳适用场景:提供降级表示,让你的类型能与期望更简单类型的接收方兼容。
swift
struct Profile: Transferable {
    var name: String
    var avatar: Image

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .profile)
        ProxyRepresentation(exporting: \.name)  // 降级方案:作为文本粘贴
    }
}
仅导出的代理(常见模式——反向转换通常不可能):
swift
ProxyRepresentation(exporting: \.name)  // Profile → String(单向)
双向代理(当反向转换有意义时):
swift
ProxyRepresentation { item in
    item.name  // 导出
} importing: { name in
    Profile(name: name)  // 导入
}

Combining Multiple Representations

组合多种表示类型

List representations in the
transferRepresentation
body. Order matters — receivers use the first representation they support.
swift
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)
    }
}
Common mistake: putting
ProxyRepresentation
first causes receivers that support both to always get the degraded version.
transferRepresentation
体中列出表示类型。顺序很重要——接收方会使用第一个它支持的表示类型。
swift
struct Profile: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        // 1. 最丰富:完整的Profile数据(支持.profile的应用)
        CodableRepresentation(contentType: .profile)
        // 2. 降级方案:纯文本(文本框、笔记应用等任何应用)
        ProxyRepresentation(exporting: \.name)
    }
}
常见错误:把
ProxyRepresentation
放在最前面会导致同时支持两种类型的接收方总是获取降级版本。

Conditional Export

条件导出

Hide a representation at runtime when conditions aren't met:
swift
DataRepresentation(contentType: .commaSeparatedText) { archive in
    try archive.toCSV()
} importing: { data in
    try Self(csvData: data)
}
.exportingCondition { archive in
    archive.supportsCSV
}
在运行时当条件不满足时隐藏某个表示类型:
swift
DataRepresentation(contentType: .commaSeparatedText) { archive in
    try archive.toCSV()
} importing: { data in
    try Self(csvData: data)
}
.exportingCondition { archive in
    archive.supportsCSV
}

Visibility

可见性

Control which processes can see a representation:
swift
CodableRepresentation(contentType: .profile)
    .visibility(.ownProcess)  // Only within this app
Options:
.all
(default),
.team
(same developer team),
.group
(same App Group, macOS),
.ownProcess
(same app only)
控制哪些进程可以看到某个表示类型:
swift
CodableRepresentation(contentType: .profile)
    .visibility(.ownProcess)  // 仅在当前应用内可见
选项:
.all
(默认)、
.team
(同一开发者团队)、
.group
(同一App Group,macOS)、
.ownProcess
(仅当前应用)

Suggested File Name

建议文件名

Hint for receivers writing to disk:
swift
FileRepresentation(contentType: .mpeg4Movie) { video in
    SentTransferredFile(video.file)
} importing: { received in
    // ...
}
.suggestedFileName("My Video.mp4")

// Or dynamic:
.suggestedFileName { video in video.title + ".mp4" }

为接收方写入磁盘时提供提示:
swift
FileRepresentation(contentType: .mpeg4Movie) { video in
    SentTransferredFile(video.file)
} importing: { received in
    // ...
}
.suggestedFileName("My Video.mp4")

// 或动态生成:
.suggestedFileName { video in video.title + ".mp4" }

Part 3: SwiftUI Surfaces

第三部分:SwiftUI交互组件

ShareLink

ShareLink

The standard sharing entry point. Accepts any
Transferable
type.
swift
// 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"))
)
Sharing multiple items with per-item previews:
swift
ShareLink(items: photos) { photo in
    SharePreview(photo.caption, image: photo.image)
}
SharePreview
initializers:
  • SharePreview("Title")
    — text only
  • SharePreview("Title", image: someImage)
    — text + full-size image
  • SharePreview("Title", icon: someIcon)
    — text + thumbnail icon
  • SharePreview("Title", image: someImage, icon: someIcon)
    — all three
Gotcha: If you omit
SharePreview
for a custom type, the share sheet shows a generic preview. Always provide one for non-trivial types.
标准分享入口。接受任何
Transferable
类型。
swift
// 简单用法:分享字符串
ShareLink(item: "Check out this app!")

// 带预览
ShareLink(
    item: photo,
    preview: SharePreview(photo.caption, image: photo.image)
)

// 分享URL并自定义预览(阻止系统获取元数据)
ShareLink(
    item: URL(string: "https://example.com")!,
    preview: SharePreview("My Site", image: Image("hero"))
)
分享多个带各自预览的项目:
swift
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
,分享面板会显示通用预览。对于非平凡类型,务必提供
SharePreview

Drag and Drop

拖拽功能

Making a view draggable:
swift
Text(profile.name)
    .draggable(profile)
With custom drag preview:
swift
Text(profile.name)
    .draggable(profile) {
        Label(profile.name, systemImage: "person")
            .padding()
            .background(.regularMaterial)
    }
Accepting drops:
swift
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
    }
Multiple item types — use an enum wrapper conforming to
Transferable
rather than stacking
.dropDestination
modifiers (stacking may cause only the outermost handler to fire):
swift
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 with reordering — combine with
.onMove
or use
draggable
/
dropDestination
for cross-container moves.
让视图支持拖拽:
swift
Text(profile.name)
    .draggable(profile)
带自定义拖拽预览:
swift
Text(profile.name)
    .draggable(profile) {
        Label(profile.name, systemImage: "person")
            .padding()
            .background(.regularMaterial)
    }
接受拖拽:
swift
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
修饰符(堆叠可能导致仅最外层处理程序触发):
swift
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
实现跨容器移动。

Clipboard (Copy/Paste)

剪贴板(复制/粘贴)

Copy support (activates Edit > Copy / Cmd+C):
swift
List(items) { item in
    Text(item.name)
}
.copyable(items)
Paste support (activates Edit > Paste / Cmd+V):
swift
List(items) { item in
    Text(item.name)
}
.pasteDestination(for: Item.self) { pasted in
    items.append(contentsOf: pasted)
} validator: { candidates in
    candidates.filter { $0.isValid }
}
The validator closure runs before the action — return an empty array to prevent the paste.
Cut support:
swift
.cuttable(for: Item.self) {
    let selected = items.filter { $0.isSelected }
    items.removeAll { $0.isSelected }
    return selected
}
PasteButton — system button that handles paste with type filtering:
swift
PasteButton(payloadType: String.self) { strings in
    notes.append(contentsOf: strings)
}
Platform difference: PasteButton auto-validates pasteboard changes on iOS but not on macOS.
Availability:
.copyable
,
.pasteDestination
, and
.cuttable
are macOS 13+ only — they do not exist on iOS. On iOS, use
PasteButton
(iOS 16+) for paste, and standard context menus or
UIPasteboard
for programmatic copy/cut.
PasteButton
is cross-platform: macOS 10.15+, iOS 16+, visionOS 1.0+.

复制支持(激活编辑>复制 / Cmd+C):
swift
List(items) { item in
    Text(item.name)
}
.copyable(items)
粘贴支持(激活编辑>粘贴 / Cmd+V):
swift
List(items) { item in
    Text(item.name)
}
.pasteDestination(for: Item.self) { pasted in
    items.append(contentsOf: pasted)
} validator: { candidates in
    candidates.filter { $0.isValid }
}
验证器闭包在操作前运行——返回空数组可阻止粘贴。
剪切支持:
swift
.cuttable(for: Item.self) {
    let selected = items.filter { $0.isSelected }
    items.removeAll { $0.isSelected }
    return selected
}
PasteButton——系统按钮,处理带类型过滤的粘贴:
swift
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+。

Part 4: UTType Declarations

第四部分:UTType声明

System Types

系统类型

Use Apple's built-in UTTypes when possible — they're already recognized across the system:
swift
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
尽可能使用苹果内置的UTType——它们已被系统全局识别:
swift
import UniformTypeIdentifiers

// 常见类型
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

Declaring Custom Types

声明自定义类型

Step 1: Declare in Swift:
swift
extension UTType {
    static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
}
Step 2: Add to Info.plist under
UTExportedTypeDeclarations
:
xml
<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>
Both are required. The Swift declaration alone makes it compile, but cross-app transfers silently fail without the Info.plist entry.
步骤1:在Swift中声明:
swift
extension UTType {
    static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
}
步骤2:在Info.plist的
UTExportedTypeDeclarations
下添加:
xml
<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条目,跨应用分享会静默失败。

Imported vs Exported Types

导入类型 vs 导出类型

  • Exported (
    exportedAs:
    ) — Your app owns this type. Use for app-specific formats.
  • Imported (
    importedAs:
    ) — Another app owns this type. Use when you want to accept their format.
  • 导出类型
    exportedAs:
    )——你的应用拥有该类型。用于应用专属格式。
  • 导入类型
    importedAs:
    )——其他应用拥有该类型。用于你需要接受其他应用格式的场景。

UTType Conformance

UTType一致性

Custom types should conform to system types for broader compatibility:
swift
// Your .recipe conforms to public.data (binary data)
// This means any receiver that accepts generic data can also accept recipes
Common conformance parents:
public.data
,
public.content
,
public.text
,
public.image

自定义类型应与系统类型保持一致性以获得更广泛的兼容性:
swift
// 你的.recipe遵循public.data(二进制数据)
// 这意味着任何接受通用数据的接收方也能接收recipe
常见的一致性父类型:
public.data
public.content
public.text
public.image

Part 5: UIKit Bridging

第五部分:UIKit桥接

NSItemProvider + Transferable

NSItemProvider + Transferable

Bridge between UIKit's
NSItemProvider
(used by
UIActivityViewController
, extensions, drag sessions) and
Transferable
:
swift
// 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
    }
}
在UIKit的
NSItemProvider
(被
UIActivityViewController
、扩展、拖拽会话使用)和
Transferable
之间桥接:
swift
// 从NSItemProvider加载Transferable类型
let provider: NSItemProvider = // 来自拖拽会话、扩展等
provider.loadTransferable(type: Profile.self) { result in
    switch result {
    case .success(let profile):
        // 使用profile
    case .failure(let error):
        // 处理错误
    }
}

When to Use UIActivityViewController

何时使用UIActivityViewController

ShareLink
covers most sharing needs. Use
UIActivityViewController
when you need:
  • Custom activity items or excluded activity types
  • UIActivityItemsConfiguration
    for lazy item provision
  • Custom
    UIActivity
    subclasses
  • Programmatic presentation control
swift
struct ShareSheet: UIViewControllerRepresentable {
    let items: [Any]

    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: items, applicationActivities: nil)
    }

    func updateUIViewController(_ vc: UIActivityViewController, context: Context) {}
}
For most apps,
ShareLink
is sufficient and preferred — it integrates with
Transferable
natively.

ShareLink
涵盖了大多数分享需求。在以下场景使用
UIActivityViewController
  • 自定义活动项目或排除的活动类型
  • 使用
    UIActivityItemsConfiguration
    实现懒加载项目
  • 自定义
    UIActivity
    子类
  • 程序化控制展示
swift
struct ShareSheet: UIViewControllerRepresentable {
    let items: [Any]

    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: items, applicationActivities: nil)
    }

    func updateUIViewController(_ vc: UIActivityViewController, context: Context) {}
}
对于大多数应用,
ShareLink
已足够且是首选——它与
Transferable
原生集成。

Part 6: Gotchas & Troubleshooting

第六部分:常见陷阱与故障排除

FileRepresentation Temporary File Lifecycle

FileRepresentation临时文件生命周期

The
received.file
URL in a
FileRepresentation
importing closure has a temporary sandbox extension. The system may revoke access after the closure returns. Always copy the file:
swift
// 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
导入闭包中的
received.file
URL拥有临时沙箱扩展。系统可能在闭包返回后撤销访问权限。务必复制文件:
swift
// 错误——文件可能变得不可访问
return Video(file: received.file)

// 正确——复制到你自己的存储目录
let dest = myAppDirectory.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: dest)
return Video(file: dest)

Async Work After File Drop

文件拖放后的异步工作

The
FileRepresentation
importing closure is synchronous — you cannot
await
inside it. Copy the file first, return the model, then do async post-processing (thumbnails, transcoding, metadata extraction) on the copied URL:
swift
// 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
}
FileRepresentation
导入闭包是同步的——你不能在其中使用
await
。先复制文件,返回模型,然后在复制后的URL上进行异步后期处理(生成缩略图、转码、提取元数据):
swift
// 错误——不能在导入闭包中使用await
FileRepresentation(importedContentType: .movie) { received in
    let dest = ...
    try FileManager.default.copyItem(at: received.file, to: dest)
    let thumbnail = await generateThumbnail(for: dest)  // ❌ 编译错误
    return VideoClip(localURL: dest, thumbnail: thumbnail)
}

//正确——立即返回,之后异步处理
// 在你的视图模型或拖放处理程序中:
.dropDestination(for: VideoClip.self) { clips, _ in
    for clip in clips {
        timeline.append(clip)
        Task {
            // clip.localURL是复制后的文件——可随时安全访问
            let thumbnail = await generateThumbnail(for: clip.localURL)
            clip.thumbnail = thumbnail
        }
    }
    return true
}

Representation Ordering

表示类型顺序

Representations are tried in declaration order. The receiver uses the first one it supports.
swift
// 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
}
表示类型按声明顺序尝试。接收方使用第一个它支持的类型。
swift
// 错误——接收方总是获取纯文本
static var transferRepresentation: some TransferRepresentation {
    ProxyRepresentation(exporting: \.name)   // ← 所有接收方都支持String
    CodableRepresentation(contentType: .profile)  // ← 永远不会被使用
}

// 正确——先声明最丰富的类型,最后声明降级类型
static var transferRepresentation: some TransferRepresentation {
    CodableRepresentation(contentType: .profile)  // ← 支持Profile的应用
    ProxyRepresentation(exporting: \.name)         // ← 其他应用的降级方案
}

Custom UTType Without Info.plist

缺少Info.plist条目的自定义UTType

If you declare
UTType(exportedAs: "com.myapp.type")
in Swift but forget the Info.plist entry:
  • In-app transfers work (same process recognizes the type)
  • Cross-app transfers silently fail (other apps can't resolve the type)
This is the most common "works in development, fails in production" issue.
如果你在Swift中声明了
UTType(exportedAs: "com.myapp.type")
但忘记添加Info.plist条目:
  • 应用内分享正常工作(同一进程能识别该类型)
  • 跨应用分享会静默失败(其他应用无法解析该类型)
这是最常见的“开发环境正常,生产环境失败”问题。

Drop Target Hit Testing

拖放目标点击测试

.dropDestination
requires the view to have a non-zero frame for hit testing. If drops aren't registering:
swift
// 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) { ... }
.dropDestination
要求视图有非零的点击测试区域。如果拖放没有响应:
swift
// 错误——Color.clear没有固有尺寸
Color.clear
    .dropDestination(for: Image.self) { ... }

// 正确——给视图设置尺寸
Color.clear
    .frame(width: 200, height: 200)
    .contentShape(Rectangle())  // 确保整个区域可被点击测试
    .dropDestination(for: Image.self) { ... }

Async Loading with loadTransferable

使用loadTransferable进行异步加载

NSItemProvider.loadTransferable
is asynchronous. Update UI on the main actor:
swift
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
        }
    }
}
NSItemProvider.loadTransferable
是异步的。在主Actor上更新UI:
swift
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 Platform Differences

PasteButton平台差异

PasteButton
auto-validates against pasteboard changes on iOS — the button enables/disables as the pasteboard content changes. On macOS, this automatic validation does not occur. If your macOS app needs dynamic paste validation, monitor
UIPasteboard.changedNotification
(UIKit) or
NSPasteboard
change count manually.

PasteButton
在iOS上会自动根据剪贴板变化验证——按钮会随剪贴板内容变化启用/禁用。在macOS上,这种自动验证不会发生。如果你的macOS应用需要动态粘贴验证,请手动监听
UIPasteboard.changedNotification
(UIKit)或
NSPasteboard
变化计数。

Resources

资源

WWDC: 2022-10062, 2022-10052, 2022-10023, 2022-10093, 2022-10095
Docs: /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
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
相关技能:axiom-photo-library, axiom-codable, axiom-swiftui-gestures, axiom-app-intents-ref