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