Skillshub axiom-transferable-ref

Use when implementing drag and drop, copy/paste, ShareLink, or ANY content sharing between apps or views - covers Transferable protocol, TransferRepresentation types, UTType declarations, SwiftUI surfaces, and NSItemProvider bridging

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-transferable-ref" ~/.claude/skills/comeonoliver-skillshub-axiom-transferable-ref && rm -rf "$T"
manifest: skills/CharlesWiltgen/Axiom/axiom-transferable-ref/SKILL.md
source content

Transferable & Content Sharing Reference

Comprehensive guide to the CoreTransferable framework and SwiftUI sharing surfaces: drag and drop, copy/paste, and 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

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?"


Part 1: Quick Reference

Decision Tree: Which 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

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()

Built-in Transferable Types

These work with zero additional code — no conformance needed:

String
,
Data
,
URL
,
AttributedString
,
Image
,
Color


Part 2: Making Types Transferable

The

Transferable
protocol has one requirement: a static
transferRepresentation
property.

CodableRepresentation

Best for: models already conforming to

Codable
. Uses JSON by default.

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):

CodableRepresentation(
    contentType: .todo,
    encoder: PropertyListEncoder(),
    decoder: PropertyListDecoder()
)

Requirement: Custom UTTypes need matching

UTExportedTypeDeclarations
in Info.plist (see Part 4).

DataRepresentation

Best for: custom binary formats where data is in memory and you control serialization.

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:

// 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
.

FileRepresentation

Best for: large payloads on disk (videos, documents, archives). Passes file URLs instead of loading bytes into memory.

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:

// 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:

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

Best for: fallback representations that let your type work with receivers expecting simpler types.

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):

ProxyRepresentation(exporting: \.name)  // Profile → String (one-way)

Bidirectional proxy (when reverse makes sense):

ProxyRepresentation { item in
    item.name  // export
} importing: { name in
    Profile(name: name)  // import
}

Combining Multiple Representations

List representations in the

transferRepresentation
body. Order matters — receivers use the first representation they support.

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.

Conditional Export

Hide a representation at runtime when conditions aren't met:

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:

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)

Suggested File Name

Hint for receivers writing to disk:

FileRepresentation(contentType: .mpeg4Movie) { video in
    SentTransferredFile(video.file)
} importing: { received in
    // ...
}
.suggestedFileName("My Video.mp4")

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

Part 3: SwiftUI Surfaces

ShareLink

The standard sharing entry point. Accepts any

Transferable
type.

// 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:

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.

Drag and Drop

Making a view draggable:

Text(profile.name)
    .draggable(profile)

With custom drag preview:

Text(profile.name)
    .draggable(profile) {
        Label(profile.name, systemImage: "person")
            .padding()
            .background(.regularMaterial)
    }

Accepting drops:

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):

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.

Clipboard (Copy/Paste)

Copy support (activates Edit > Copy / Cmd+C):

List(items) { item in
    Text(item.name)
}
.copyable(items)

Paste support (activates Edit > Paste / 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 }
}

The validator closure runs before the action — return an empty array to prevent the paste.

Cut support:

.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:

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+.


Part 4: UTType Declarations

System Types

Use Apple's built-in UTTypes when possible — they're already recognized across the system:

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

Declaring Custom Types

Step 1: Declare in Swift:

extension UTType {
    static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
}

Step 2: Add to Info.plist under

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>

Both are required. The Swift declaration alone makes it compile, but cross-app transfers silently fail without the Info.plist entry.

Imported vs Exported Types

  • 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.

UTType Conformance

Custom types should conform to system types for broader compatibility:

// 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


Part 5: UIKit Bridging

NSItemProvider + Transferable

Bridge between UIKit's

NSItemProvider
(used by
UIActivityViewController
, extensions, drag sessions) and
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
    }
}

When to Use 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
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.


Part 6: Gotchas & Troubleshooting

FileRepresentation Temporary File Lifecycle

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:

// 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)

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:

// 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
}

Representation Ordering

Representations are tried in declaration order. The receiver uses the first one it supports.

// 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
}

Custom UTType Without Info.plist

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.

Drop Target Hit Testing

.dropDestination
requires the view to have a non-zero frame for hit testing. If drops aren't registering:

// 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) { ... }

Async Loading with loadTransferable

NSItemProvider.loadTransferable
is asynchronous. Update UI on the main actor:

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
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.


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