```html SwiftUI: How to Drag and Drop (iOS 17+, 2026)

How to drag and drop in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: Drag / Drop / Transferable Updated: May 11, 2026
TL;DR

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

  1. Transferable conformanceFruit adopts Transferable via CodableRepresentation(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. ProxyRepresentation for images) inside transferRepresentation to support different targets.
  2. .draggable modifierFruitChip is 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.
  3. .dropDestination modifierBasketView uses .dropDestination(for: Fruit.self). The action closure receives the array of dropped items and the drop point; returning true signals the system that the drop was accepted. SwiftUI filters out payload types that don't match.
  4. isTargeted callback — The second trailing closure isTargeted: fires when a drag enters or leaves the drop zone. Storing this in @State and animating the background colour gives users clear visual feedback during the drag interaction.
  5. Accessibility.accessibilityLabel on both FruitChip and BasketView provides 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

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.

```