How to drag and drop in SwiftUI
Conform your model to Transferable, attach .draggable(_:) to the source view, and add .dropDestination(for:action:) to the target. That's the entire drag-and-drop loop in SwiftUI on iOS 17+.
struct Fruit: Transferable {
var name: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .plainText)
}
}
// Drag source
Text(fruit.name)
.draggable(fruit)
// Drop target
RoundedRectangle(cornerRadius: 12)
.dropDestination(for: Fruit.self) { items, _ in
basket.append(contentsOf: items)
return true
}
Full implementation
The example below builds a two-column board: a source list of fruits on the left and a basket drop zone on the right. The Fruit model adopts Transferable via CodableRepresentation, which lets SwiftUI serialise the value automatically. Each fruit chip is marked as draggable, and the basket accepts drops of type Fruit.self, appending every received item to its local state array. A subtle visual cue (isTargeted) highlights the basket while a drag hovers over it.
import SwiftUI
import UniformTypeIdentifiers
// MARK: – Model
struct Fruit: Codable, Identifiable, Transferable {
var id = UUID()
var name: String
var emoji: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .plainText)
}
}
// MARK: – Views
struct DragDropBoardView: View {
let available: [Fruit] = [
Fruit(name: "Apple", emoji: "🍎"),
Fruit(name: "Banana", emoji: "🍌"),
Fruit(name: "Cherry", emoji: "🍒"),
Fruit(name: "Grape", emoji: "🍇"),
Fruit(name: "Mango", emoji: "🥭"),
]
@State private var basket: [Fruit] = []
var body: some View {
HStack(alignment: .top, spacing: 20) {
// Source list
VStack(alignment: .leading, spacing: 12) {
Text("Available")
.font(.headline)
ForEach(available) { fruit in
FruitChip(fruit: fruit)
.draggable(fruit) {
// Custom drag preview
FruitChip(fruit: fruit)
.opacity(0.85)
}
}
}
.frame(maxWidth: .infinity)
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
// Drop target
BasketView(basket: $basket)
}
.padding()
}
}
struct FruitChip: View {
let fruit: Fruit
var body: some View {
Label(fruit.name, title: { Text(fruit.name) })
.labelStyle(.titleAndIcon)
.font(.subheadline.weight(.medium))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(Color.accentColor.opacity(0.12), in: Capsule())
.accessibilityLabel("\(fruit.emoji) \(fruit.name), draggable")
}
}
struct BasketView: View {
@Binding var basket: [Fruit]
@State private var isTargeted = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Basket (\(basket.count))")
.font(.headline)
if basket.isEmpty {
Text("Drop fruits here")
.font(.subheadline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, minHeight: 80)
} else {
ForEach(basket) { fruit in
Text("\(fruit.emoji) \(fruit.name)")
.font(.subheadline)
}
}
}
.frame(maxWidth: .infinity, minHeight: 160, alignment: .topLeading)
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(isTargeted
? Color.accentColor.opacity(0.18)
: Color(.secondarySystemBackground))
.animation(.easeInOut(duration: 0.2), value: isTargeted)
)
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder(isTargeted ? Color.accentColor : .clear, lineWidth: 2)
)
.dropDestination(for: Fruit.self) { items, _ in
basket.append(contentsOf: items)
return true
} isTargeted: { targeted in
isTargeted = targeted
}
.accessibilityLabel("Basket, drop zone, \(basket.count) fruits")
}
}
// MARK: – Preview
#Preview {
DragDropBoardView()
}
How it works
-
Transferable conformance —
FruitadoptsTransferableviaCodableRepresentation(contentType: .plainText). SwiftUI uses this representation to encode the value when a drag starts and decode it when a drop lands. You can stack multiple representations (e.g.ProxyRepresentationfor images) insidetransferRepresentationto support different targets. -
.draggable modifier —
FruitChipis wrapped with.draggable(fruit). The trailing closure provides an optional custom drag preview rendered as a floating snapshot. Without the closure, SwiftUI renders a default preview from the view itself. -
.dropDestination modifier —
BasketViewuses.dropDestination(for: Fruit.self). Theactionclosure receives the array of dropped items and the drop point; returningtruesignals the system that the drop was accepted. SwiftUI filters out payload types that don't match. -
isTargeted callback — The second trailing closure
isTargeted:fires when a drag enters or leaves the drop zone. Storing this in@Stateand animating the background colour gives users clear visual feedback during the drag interaction. -
Accessibility —
.accessibilityLabelon bothFruitChipandBasketViewprovides VoiceOver context. On iOS 17+, VoiceOver supports a dedicated drag-and-drop gesture independently of touch, so labelling both source and destination is essential for full accessibility.
Variants
Reorderable list with .onMove and drag handles
struct ReorderableListView: View {
@State private var items = ["Alpha", "Beta", "Gamma", "Delta"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove { source, destination in
items.move(fromOffsets: source, toOffset: destination)
}
}
.toolbar {
EditButton()
}
}
}
#Preview { ReorderableListView() }
Use .onMove inside a ForEach wrapped in a List for in-list reordering without needing full Transferable conformance. Pair with EditButton to reveal drag handles, or set .environment(\.editMode, .constant(.active)) to keep them always visible.
Dragging images with FileRepresentation
struct DraggablePhoto: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .image) { photo in
SentTransferredFile(photo.url)
} importing: { received in
let copy = URL.documentsDirectory
.appending(path: received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: copy)
return DraggablePhoto(url: copy)
}
}
}
FileRepresentation is the right choice when the payload is a file on disk — such as an image or document — rather than a small in-memory value. On iPadOS, this enables cross-app drag-and-drop into Files or Photos without any extra entitlements.
Common pitfalls
-
iOS 16 vs 17: The
.dropDestination(for:action:isTargeted:)overload with theisTargetedclosure was introduced in iOS 16, but severalTransferablerepresentations (e.g.DataRepresentationwith customUTType) behave differently before iOS 17. Always test on your minimum deployment target and gate on#available(iOS 17, *)if you support iOS 16. -
Transferable must be Codable or Sendable: If your model contains non-
Codableproperties (e.g. aUIImage), you cannot useCodableRepresentationdirectly. UseDataRepresentationwith a custom UTType and manual encode/decode logic, or break the image out into a separateTransferabletype. -
Performance with large payloads: Encoding happens on the main thread by default during the drag lift. For large data (e.g. HD images), use
FileRepresentationwith a file URL to avoid blocking the UI. Also avoid putting@Publishedarrays that trigger full redraws inside thedropDestinationaction without batching the updates. -
Missing VoiceOver support: SwiftUI's
.draggabledoes not automatically expose an accessibility action. Add.accessibilityAction(.init("Drag")) { ... }or use the system long-press + drag VoiceOver gesture, and ensure all drop zones have descriptive.accessibilityLabelvalues so VoiceOver users know they are valid targets.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement drag and drop in SwiftUI for iOS 17+. Use Transferable, .draggable, and .dropDestination. Make it accessible (VoiceOver labels on both source and target). Add a #Preview with realistic sample data. Show an isTargeted visual highlight on the drop zone.
In Soarias's Build phase, paste this prompt directly into the Claude Code panel alongside your feature file — Soarias will wire the generated component into your existing @Observable model and keep your project compiling without leaving your Mac.
Related
FAQ
Does this work on iOS 16?
The Transferable protocol and .draggable / .dropDestination modifiers were introduced in iOS 16, so the basic pattern compiles and runs on iOS 16. However, some TransferRepresentation types (like CodableRepresentation with custom UTTypes) have subtle bugs fixed in iOS 17, and the isTargeted closure overload requires iOS 16.0+. If you target iOS 16 specifically, test thoroughly and prefer DataRepresentation with explicit UTType declarations.
Can I drag items between two different apps on iPadOS?
Yes. Cross-app drag-and-drop on iPadOS works automatically as long as both apps declare compatible UTTypes in their transferRepresentation. For text (.plainText) and images (.image), no additional entitlements are needed. For custom app-defined types, both apps must declare the same UTType identifier in their Info.plist. On iPhone (iOS 17 with Stage Manager), the same mechanism applies.
What is the UIKit equivalent?
In UIKit, drag-and-drop is implemented via UIDragInteraction and UIDropInteraction delegates on any UIView, or through UITableView / UICollectionView data-source drag delegate methods (UITableViewDragDelegate, UICollectionViewDropDelegate). The payload is wrapped in NSItemProvider rather than Transferable. The SwiftUI Transferable system bridges to NSItemProvider under the hood, so you can interop with UIKit-based drop targets by implementing NSItemProviderWriting / NSItemProviderReading on your model.
Last reviewed: 2026-05-11 by the Soarias team.