---
name: axiom-transferable-ref
description: 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
license: MIT
metadata:
version: "1.0.0"
---
# 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 / Symptom | Cause | Fix |
|-----------------|-------|-----|
| "Type does not conform to Transferable" | Missing `transferRepresentation` | Add `static var transferRepresentation: some TransferRepresentation` |
| Drop works in-app but not across apps | Custom UTType not declared in Info.plist | Add `UTExportedTypeDeclarations` entry |
| Receiver always gets plain text instead of rich type | ProxyRepresentation listed before CodableRepresentation | Reorder: richest representation first |
| FileRepresentation crashes with "file not found" | Receiver didn't copy file before sandbox extension expired | Copy to app storage in the importing closure |
| PasteButton always disabled | Pasteboard doesn't contain matching Transferable type | Check UTType conformance; verify the pasted data matches |
| ShareLink shows generic preview | No `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 area | Verify `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.
```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).
### 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`.
### 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)
}
```
### 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
}
```
### 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.
### 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
}
```
### 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)
### 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" }
```
---
## Part 3: SwiftUI Surfaces
### 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.
### 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.
### 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+.
---
## Part 4: UTType Declarations
### 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
```
### 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
UTExportedTypeDeclarations
UTTypeIdentifier
com.myapp.recipe
UTTypeDescription
Recipe
UTTypeConformsTo
public.data
UTTypeTagSpecification
public.filename-extension
recipe
```
**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:
```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`
---
## Part 5: UIKit Bridging
### 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
}
}
```
### 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
```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.
---
## 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:
```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)
```
### 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
}
```
### 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
}
```
### 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:
```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) { ... }
```
### Async Loading with 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
}
}
}
```
### 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