Build a Drag and Drop SwiftUI Flow
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: