Skip to Content
Course content

227: Practice Exercise: Building a Drag-and-Drop Interface in SwiftUI

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was trying to build a custom Trello-style kanban board. He had spent three days fighting with DragGesture, manually calculating offsets and trying to figure out which list item the user was hovering over by comparing global coordinates. It was a nightmare to read, and it felt "jittery" on the screen. I told him to scrap it all and look at the native Transferable APIs. He was skeptical—he thought native drag-and-drop was only for moving files between apps—but once he saw how SwiftUI handles the data handoff, he finished the feature in an afternoon.

The trick is to stop thinking about the *movement* of the view and start thinking about the *transfer* of data. In SwiftUI, we don't move the UI element itself; we tell the system, "This piece of data is now floating," and then we tell a destination, "If you see this type of data, here is how to handle it."

Defining the Transferable Payload

To make an element draggable, it has to conform to the Transferable protocol. This is where most people get stuck because they try to make the entire View draggable. You don't do that. Instead, you make your data model transferable. If you're moving a Task object, that Task needs to know how to represent itself as something the system can move—usually a string or a custom data blob.

struct TodoTask: Identifiable, Codable, Transferable {
    let id: UUID
    var title: String
    
    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation()
    }
}

By using CodableRepresentation, you're telling SwiftUI to handle the serialization for you. Once your model is set up, adding the .draggable() modifier to your view is a one-liner. I usually recommend adding a slight visual cue, like a subtle shadow or scale effect, so the user knows the item is "lifted," though the system provides a default ghost image automatically.

Handling the Drop Destination

Now that you've got data floating in the ether, you need a place for it to land. The .dropDestination modifier is your best friend here. It requires two things: the type of data you're expecting and a closure that executes when the drop happens.

Here is the part where you'll actually update your state. When the drop occurs, the closure gives you the transferred item. You then find that item in your source list, remove it, and append it to the destination list. I've found that if you don't wrap this logic in withAnimation, the transition feels abrupt and robotic. Always animate your state changes during a drop; it's the difference between an app that feels "engineered" and one that feels "polished."

.dropDestination(for: TodoTask.self) { items, location in
    guard let task = items.first else { return false }
    
    withAnimation(.spring()) {
        // Logic to move task from 'todoList' to 'doneList'
        todoList.removeAll { $0.id == task.id }
        doneList.append(task)
    }
    return true
}

One quick tip: keep your drop zones large. Nothing frustrates a user more than trying to drop an item into a column and having it "bounce back" because they missed the target by two pixels. Make the entire container the drop destination, not just the individual cells.




📋 Practical Task

Exercise: The Kanban Task Shifter

Build a simple two-column task organizer. You will need to implement the following:

  • Create a TaskItem struct that conforms to Identifiable, Codable, and Transferable.
  • Set up two arrays of TaskItem (e.g., todoTasks and completedTasks) inside a ViewModel or as @State variables.
  • Create a view with two VStack columns. Each column should display its respective list of tasks.
  • Apply the .draggable() modifier to the task rows so they can be picked up.
  • Apply the .dropDestination() modifier to each column. When a task is dropped into the "Completed" column, it should be removed from the "Todo" list and added to the "Completed" list (and vice versa).
  • Ensure the movement is wrapped in a withAnimation block to provide a smooth visual transition.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.