If you've been building SwiftUI apps for a while, you've probably wrestled with moving data between views — dragging items around, copying to the clipboard, sharing things with other apps. It used to mean juggling NSItemProvider, UIPasteboard, and UIActivityViewController all at once, and honestly, it was kind of a mess.
That's where the Transferable protocol comes in. Introduced in the CoreTransferable framework alongside iOS 16, it gives you a single, declarative way to handle drag and drop, copy and paste, sharing via ShareLink, and file import/export. You conform your model to one protocol and unlock all of these capabilities at once. It's one of those things that, once you start using it, makes you wonder how you ever lived without it.
So, let's dive in and see how it all works — with real code examples updated for Swift 6 and iOS 26.
What Is the Transferable Protocol?
Transferable is a model-layer protocol defined in CoreTransferable. Its job is pretty straightforward: it describes how your types get serialized into binary data for transfer, and how they get deserialized back on the receiving end. Think of it as a Swift-native, declarative replacement for the old NSItemProvider pattern.
Once a type conforms to Transferable, it just works with every SwiftUI API that moves data between contexts:
- Drag and drop —
.draggable()and.dropDestination() - Copy, cut, and paste —
.copyable(),.cuttable(),.pasteable(), andPasteButton - Sharing —
ShareLink - File import/export —
.fileImporter()and.fileExporter()
The protocol has a single requirement: a static transferRepresentation property that returns one or more representations describing how your data converts to and from binary. That's it.
import CoreTransferable
struct Note: Transferable {
var title: String
var body: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
}
}
Built-In Transferable Types
Here's the good news — you don't have to write conformances for everything. A bunch of standard Swift and SwiftUI types already conform to Transferable out of the box:
StringandAttributedStringURLDataImageColor
This means a basic ShareLink for a URL is literally one line:
ShareLink(item: URL(string: "https://swiftcrafted.com")!)
And making an image draggable? Equally simple:
Image(systemName: "star.fill")
.draggable(Image(systemName: "star.fill"))
Understanding Transfer Representations
The Transferable protocol needs you to describe how your data converts to and from binary. Apple provides four built-in representation types, and picking the right one matters more than you'd think.
CodableRepresentation
This is your go-to choice when your type already conforms to Codable. It's the simplest option — it automatically serializes your model to JSON and deserializes it back, no extra work needed.
import CoreTransferable
import UniformTypeIdentifiers
struct Task: Codable, Transferable {
var title: String
var isCompleted: Bool
var priority: Int
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
}
}
By default, it uses JSONEncoder and JSONDecoder. If you need a different format, you can pass custom encoder and decoder instances.
DataRepresentation
Use DataRepresentation when you need full control over how your type converts to and from raw Data. This is ideal for types that aren't Codable or that require a specific binary format.
struct Sketch: Transferable {
var strokes: [CGPoint]
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .png) { sketch in
try sketch.renderToPNGData()
} importing: { data in
try Sketch(pngData: data)
}
}
}
One thing to keep in mind: DataRepresentation loads the entire payload into memory. For large files, you'll want FileRepresentation instead.
FileRepresentation
This one's for when your data is backed by a file on disk, or when the payload is just too large to hold in memory comfortably. The system transfers the file URL, and the receiver can lazily load the contents as needed.
struct VideoClip: Transferable {
let fileURL: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .mpeg4Movie) { clip in
SentTransferredFile(clip.fileURL)
} importing: { received in
let destination = FileManager.default.temporaryDirectory
.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: destination)
return VideoClip(fileURL: destination)
}
}
}
The system automatically grants the receiver a temporary sandbox extension to access the file, which makes this safe for inter-app transfers. Pretty neat, honestly.
ProxyRepresentation
ProxyRepresentation lets your type piggyback on another type's Transferable conformance. This is super useful for providing a simpler fallback — like exposing just the title of a document as a plain String when someone pastes it into a text field.
struct Article: Codable, Transferable {
var title: String
var content: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
ProxyRepresentation(exporting: \.title)
}
}
In this example, apps that understand the custom JSON format get the full article. A plain text editor? It just receives the title. Best of both worlds.
Defining Custom Uniform Type Identifiers
When you transfer custom data, you need a Uniform Type Identifier (UTType) that tells the receiver what kind of data it's getting. Apple provides built-in UTTypes for standard formats like JSON, PNG, and plain text. But for your own custom formats, you'll need to define new ones.
Step 1: Declare the UTType in Code
import UniformTypeIdentifiers
extension UTType {
static var recipe: UTType {
UTType(exportedAs: "com.yourcompany.recipe")
}
}
Use exportedAs: for types your app owns. Use importedAs: for types defined by other apps or standards that you want to support.
Step 2: Register in Info.plist
For your custom UTType to work across apps (and with drag and drop from Finder or Files), you need to register it in your target's Info.plist under Exported Type Identifiers. Specify the identifier, a description, and the conforming types — for example, public.data and public.json. You can also add a preferred file extension so the system knows which files belong to your type.
This step is easy to forget, and it'll bite you later if you skip it.
Step 3: Use It in Your Transferable Conformance
struct Recipe: Codable, Transferable {
var name: String
var ingredients: [String]
var steps: [String]
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .recipe)
}
}
Drag and Drop in SwiftUI
Drag and drop is probably the most visible (and fun) application of Transferable. SwiftUI gives you two key modifiers: .draggable() to make a view draggable and .dropDestination() to accept dropped content.
Making a View Draggable
Just attach the .draggable() modifier to any view and pass a Transferable value:
struct BookmarkRow: View {
let bookmark: Bookmark
var body: some View {
HStack {
Image(systemName: "link")
Text(bookmark.title)
}
.draggable(bookmark.url)
}
}
You can also provide a custom drag preview with a trailing closure:
Text(bookmark.title)
.draggable(bookmark.url) {
Label(bookmark.title, systemImage: "link.circle.fill")
.padding(8)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
}
Accepting Drops
Use .dropDestination() to handle incoming data. The closure receives two parameters: an array of the transferred items and the drop location within the view.
struct BookmarkList: View {
@State private var bookmarks: [URL] = []
var body: some View {
List {
ForEach(bookmarks, id: \.self) { url in
Text(url.absoluteString)
.draggable(url)
}
}
.dropDestination(for: URL.self) { droppedURLs, location in
bookmarks.append(contentsOf: droppedURLs)
return true
} isTargeted: { isTargeted in
// Use this to show visual feedback (e.g., highlight border)
}
}
}
That isTargeted closure fires whenever a drag enters or exits your drop zone — perfect for showing visual feedback like a highlighted border or a pulsing animation.
Drag and Drop with Custom Types
For custom types, you just make your model conform to Transferable and drag and drop works automatically. This is where it really starts to feel magical:
struct PhotoItem: Identifiable, Codable, Transferable {
let id: UUID
var name: String
var imageName: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
ProxyRepresentation(exporting: \.name) // Fallback for text editors
}
}
struct PhotoGrid: View {
@State private var photos: [PhotoItem] = PhotoItem.sampleData
@State private var favorites: [PhotoItem] = []
var body: some View {
HStack {
VStack {
Text("All Photos").font(.headline)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
ForEach(photos) { photo in
Image(systemName: photo.imageName)
.font(.largeTitle)
.frame(width: 80, height: 80)
.background(.quaternary)
.clipShape(RoundedRectangle(cornerRadius: 8))
.draggable(photo)
}
}
}
Divider()
VStack {
Text("Favorites").font(.headline)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
ForEach(favorites) { photo in
Image(systemName: photo.imageName)
.font(.largeTitle)
.frame(width: 80, height: 80)
.background(.yellow.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.dropDestination(for: PhotoItem.self) { items, _ in
favorites.append(contentsOf: items)
return true
}
}
}
.padding()
}
}
Copy, Cut, and Paste
SwiftUI also gives you dedicated modifiers for clipboard operations, and they integrate seamlessly with Transferable.
The .copyable(), .cuttable(), and .pasteable() Modifiers
These modifiers respond to the system's standard Copy, Cut, and Paste keyboard shortcuts and menu commands. Here's how they look in practice:
struct NoteListView: View {
@State private var notes: [Note] = Note.sampleData
@State private var selection: Set<Note.ID> = []
var body: some View {
List(selection: $selection) {
ForEach(notes) { note in
Text(note.title)
}
}
.copyable(selectedNotes)
.cuttable(for: Note.self) {
let cut = selectedNotes
notes.removeAll { selection.contains($0.id) }
return cut
}
.pasteable(for: Note.self) { pasted in
notes.append(contentsOf: pasted)
}
}
private var selectedNotes: [Note] {
notes.filter { selection.contains($0.id) }
}
}
When users press Cmd+C, the .copyable() modifier serializes the selected notes using their Transferable conformance and places them on the clipboard. Cmd+V triggers .pasteable(), which deserializes the clipboard contents and passes them to your closure. Simple as that.
PasteButton
PasteButton is a system-styled button that reads content from the clipboard when tapped. Here's the key difference from using UIPasteboard directly: PasteButton automatically handles the privacy prompt that iOS shows when an app accesses the clipboard. No more "App X pasted from App Y" banners scaring your users.
struct PasteView: View {
@State private var pastedURLs: [URL] = []
var body: some View {
VStack {
PasteButton(payloadType: URL.self) { urls in
pastedURLs.append(contentsOf: urls)
}
List(pastedURLs, id: \.self) { url in
Link(url.absoluteString, destination: url)
}
}
}
}
The button automatically dims itself when the clipboard doesn't contain matching content, which is a nice UX touch.
Sharing with ShareLink
ShareLink replaces the need to present UIActivityViewController manually. It works with any Transferable type and renders as a standard share button. If you've ever written the boilerplate for presenting an activity view controller, you'll appreciate how clean this is.
Basic Sharing
// Share a URL
ShareLink(item: URL(string: "https://swiftcrafted.com")!)
// Share a URL with subject and message
ShareLink(
item: URL(string: "https://swiftcrafted.com")!,
subject: Text("Check out Swift Crafted"),
message: Text("Great SwiftUI tutorials and guides")
)
// Custom button label
ShareLink(item: URL(string: "https://swiftcrafted.com")!) {
Label("Share", systemImage: "square.and.arrow.up")
}
Sharing Custom Types with Preview
When sharing custom types, you can provide a SharePreview to give the share sheet a title and optional image:
struct Photo: Identifiable, Transferable {
let id: UUID
var caption: String
var image: Image
var imageData: Data
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .png) { photo in
photo.imageData
}
ProxyRepresentation(exporting: \.caption)
}
}
struct PhotoDetailView: View {
let photo: Photo
var body: some View {
VStack {
photo.image
.resizable()
.scaledToFit()
ShareLink(
item: photo,
preview: SharePreview(
photo.caption,
image: photo.image
)
)
}
}
}
Sharing Multiple Items
ShareLink also supports sharing collections. Each item gets its own preview:
ShareLink(items: photos) { photo in
SharePreview(photo.caption, image: photo.image)
}
File Import and Export
Transferable also powers SwiftUI's file handling modifiers. If you need to open or save documents, this is the declarative way to do it.
Importing Files
struct DocumentPickerView: View {
@State private var showImporter = false
@State private var importedText = ""
var body: some View {
VStack {
Button("Import Document") {
showImporter = true
}
Text(importedText)
}
.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [.plainText, .json]
) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
if let content = try? String(contentsOf: url) {
importedText = content
}
case .failure(let error):
print("Import failed: \(error.localizedDescription)")
}
}
}
}
That startAccessingSecurityScopedResource() call is critical. Without it, reading files from outside your app's sandbox will fail with a permissions error. I've been burned by this more times than I'd like to admit.
Exporting Files
struct ExportView: View {
@State private var showExporter = false
let document = TextDocument(text: "Hello, SwiftUI!")
var body: some View {
Button("Export Document") {
showExporter = true
}
.fileExporter(
isPresented: $showExporter,
document: document,
contentType: .plainText,
defaultFilename: "MyDocument.txt"
) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print("Export failed: \(error.localizedDescription)")
}
}
}
}
struct TextDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var text: String
init(text: String) { self.text = text }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
text = String(decoding: data, as: UTF8.self)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: Data(text.utf8))
}
}
Multiple Representations and Priority
This is honestly one of my favorite features of Transferable. You can provide multiple representations for the same type, and the system automatically picks the best one that the receiver understands.
struct RichNote: Codable, Transferable {
var title: String
var htmlContent: String
var plainText: String
var attachmentURL: URL?
static var transferRepresentation: some TransferRepresentation {
// Full fidelity: custom Codable format
CodableRepresentation(contentType: .json)
// Medium fidelity: HTML string
DataRepresentation(exportedContentType: .html) { note in
Data(note.htmlContent.utf8)
}
// Low fidelity: plain text fallback
ProxyRepresentation(exporting: \.plainText)
}
}
The order matters here. Place the highest-fidelity representation first. An app that understands your custom JSON format gets the full data. A rich text editor that supports HTML gets the formatted version. And a plain text field? It just gets the plain text.
This cascading behavior is one of the key advantages of Transferable over manual serialization. You define all your representations in one place, and the system negotiates the best match automatically. No more writing conditional logic to figure out what the receiver supports.
Practical Example: A Reusable Color Palette App
Let's put everything together with a real-world example. We'll build a color palette app where users can drag colors between grids, copy colors to the clipboard, and share their palette with others.
import SwiftUI
import CoreTransferable
import UniformTypeIdentifiers
// MARK: - Model
struct PaletteColor: Identifiable, Codable, Hashable, Transferable {
let id: UUID
var name: String
var hex: String
init(name: String, hex: String) {
self.id = UUID()
self.name = name
self.hex = hex
}
var color: Color {
Color(hex: hex)
}
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
ProxyRepresentation(exporting: \.hex)
}
}
// MARK: - View
struct PaletteView: View {
@State private var colors: [PaletteColor] = [
PaletteColor(name: "Ocean Blue", hex: "#0077B6"),
PaletteColor(name: "Sunset Orange", hex: "#FF6B35"),
PaletteColor(name: "Forest Green", hex: "#2D6A4F"),
PaletteColor(name: "Royal Purple", hex: "#7B2CBF"),
]
@State private var favorites: [PaletteColor] = []
var body: some View {
NavigationStack {
VStack(spacing: 20) {
// Main palette — draggable, copyable, shareable
VStack(alignment: .leading) {
Text("Palette").font(.headline)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))]) {
ForEach(colors) { item in
item.color
.frame(width: 70, height: 70)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay {
Text(item.name)
.font(.caption2)
.foregroundStyle(.white)
}
.draggable(item)
}
}
.copyable(colors)
}
Divider()
// Favorites — drop target
VStack(alignment: .leading) {
Text("Favorites").font(.headline)
if favorites.isEmpty {
ContentUnavailableView(
"Drop Colors Here",
systemImage: "heart.circle.fill"
)
.frame(height: 100)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))]) {
ForEach(favorites) { item in
item.color
.frame(width: 70, height: 70)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
}
.dropDestination(for: PaletteColor.self) { items, _ in
favorites.append(contentsOf: items)
return true
}
// Share the full palette
ShareLink(
items: colors,
preview: { SharePreview($0.name) }
)
}
.padding()
.navigationTitle("Color Palette")
.pasteable(for: PaletteColor.self) { pasted in
colors.append(contentsOf: pasted)
}
}
}
}
This single view demonstrates drag and drop between grids, copy/paste via keyboard shortcuts, and sharing through the share sheet — all powered by one Transferable conformance on PaletteColor. That's the beauty of this protocol: you write the conformance once and everything else just falls into place.
Swift 6 Sendability Considerations
If you're working with Swift 6 (and you probably should be at this point), there's an important detail to keep in mind. The compiler now enforces strict concurrency checking, and since Transferable operations can cross concurrency boundaries — the system might serialize your data on a background thread — your types need to conform to Sendable.
For simple value types (structs and enums with Sendable properties), this conformance is implicit. You don't have to do anything extra. But for types with mutable reference properties, you may need to annotate them explicitly:
struct SafeNote: Codable, Sendable, Transferable {
let id: UUID
let title: String
let content: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .json)
}
}
The simplest path to full Swift 6 compatibility? Keep your Transferable types as immutable value types with let properties wherever possible.
Common Pitfalls and Troubleshooting
Before you ship your Transferable implementation, watch out for these gotchas that trip up a lot of developers (including me, at various points):
- SF Symbol drag behavior: When dragging an
Imagecreated from an SF Symbol, SwiftUI transfers the rasterized pixel data, not the vector. The dropped image won't respond to.font()or.foregroundStyle()on the receiving end. - FileRepresentation without ProxyRepresentation: On macOS, using only
FileRepresentationcan cause drag and drop to fail in Finder. Always add aProxyRepresentationthat returns the file URL as a fallback. - Representation ordering: Always place higher-fidelity representations first. The system picks the first one the receiver supports, so if you put your plain text fallback at the top, that's what everyone gets.
- Missing Info.plist entry: Custom UTTypes must be registered in your target's Info.plist under Exported or Imported Type Identifiers. Without this registration, cross-app transfers fail silently — no errors, just nothing happens. Frustrating to debug.
- Security-scoped URLs: When importing files with
.fileImporter(), always callstartAccessingSecurityScopedResource()andstopAccessingSecurityScopedResource(). Skip this and you'll get mysterious permission errors.
Frequently Asked Questions
What types conform to Transferable by default in SwiftUI?
String, AttributedString, URL, Data, Image, and Color all conform to Transferable out of the box. You can use them directly with .draggable(), ShareLink, PasteButton, and other APIs without any additional conformance code.
What is the difference between CodableRepresentation, DataRepresentation, and FileRepresentation?
CodableRepresentation is for types that already conform to Codable — it handles serialization automatically using JSON by default. DataRepresentation gives you full control over the binary conversion, which is ideal for custom formats or small in-memory payloads. FileRepresentation is for large data that lives on disk (like videos or big documents), where you want to transfer file URLs instead of loading everything into memory.
How do I add drag and drop support to a SwiftUI List?
Attach .draggable() to individual row views, passing a Transferable value. For the drop target, attach .dropDestination(for:action:isTargeted:) to the List or a container view. The isTargeted closure lets you show visual feedback when a drag enters the drop zone.
Can I use Transferable on macOS, watchOS, and tvOS?
Yes — Transferable and CoreTransferable are available across all Apple platforms: iOS, iPadOS, macOS, watchOS, tvOS, and visionOS. That said, drag and drop interactions are most relevant on iOS, iPadOS, macOS, and visionOS where pointer or touch-based dragging is available.
How do I share a rendered SwiftUI view as an image?
Use ImageRenderer to convert a SwiftUI view into a UIImage or CGImage, then convert it to PNG data and share it through a ShareLink with a custom Transferable type that uses DataRepresentation(exportedContentType: .png). This lets you generate shareable images from any SwiftUI view on demand.