swiftui · tutorial · components · layout

Build a Drag and Drop SwiftUI Flow

Published October 5, 2026 · 3 min read · intermediate

Goal

By the end of this tutorial, you will have a simple drag and drop flow that keeps state ownership in one place and updates the UI predictably.

Requirements

  • Basic SwiftUI knowledge
  • Xcode 15+
  • A list of items that can be reordered or moved

The use case

Drag and drop shows up in list organization, card sorting, and feature dashboards. The important part is not the gesture itself. It is deciding which view owns the order and which views simply reflect it.

Step 1 - Start with owned data

Keep the item order in one place.

import SwiftUI

struct ReorderScreen: View {
    @State private var items = ["One", "Two", "Three"]

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
    }
}

What changed:

  • the screen owns the ordering
  • the list is still just rendering data

Verification:

  • the list displays all items
  • the source of truth is obvious

Step 2 - Add a draggable row

Make each row participate in drag and drop.

struct DraggableRow: View {
    let item: String

    var body: some View {
        Text(item)
            .padding(.vertical, 8)
            .onDrag {
                NSItemProvider(object: item as NSString)
            }
    }
}

What changed:

  • the row advertises drag data
  • the row still does not own the collection

Verification:

  • the row can start a drag
  • the item value is carried with the drag

Step 3 - Reorder the collection

Move the item in the owning view when the drop happens.

func moveItem(from source: Int, to destination: Int) {
    items.move(fromOffsets: IndexSet(integer: source), toOffset: destination)
}

What changed:

  • the state mutation stays at the screen boundary
  • the rows remain dumb and reusable

Verification:

  • reordering updates the list
  • the items array remains the single source of truth

Result

You now have a drag and drop flow where the UI and the data model stay aligned. That makes the interaction easier to extend later if you want persistence, sectioning, or custom drop targets.

Common SwiftUI pitfalls

  • Mistake: letting each row own its own order state
    Why it happens: the row feels like the interactive unit
    How to fix it: keep ordering in the parent screen
  • Mistake: combining drag logic and persistence in one step
    Why it happens: it feels faster to wire everything together
    How to fix it: get the interaction working first, then add saving

When NOT to use this

Do not add drag and drop just because the platform supports it. If a simple edit control or reorder button is clearer, use that instead.

Takeaway

If you remember one thing: Drag and drop should move data at the owning boundary, not mutate the row that merely displays it.

Next steps

This is the right continuation because it connects the interaction pattern back to layout, identity, and reusable SwiftUI components: