Add drag and drop support to UITableView

This article is a clearer UIKit walkthrough for UITableView drag and drop. The example app tracks which cat owns which toy, then lets you drag a toy out of one table and drop it into another. The useful parts are the same even if your app is different: enable dragging, wrap the row's data in an NSItemProvider, accept a drop, recover the payload, and then update the real backing model.

Demo GIF showing a toy being dragged from one table view and dropped into another

The point of drag and drop is not just movement on screen. It is a clean handoff of row data from one table to another.

This article frames the feature through a playful example, but the implementation pattern is broadly useful. Once UITableView drag and drop is wired in, users can long-press a cell, drag it away from its current list, and hand that data to another list that knows how to accept it.

In this tutorial, the transferred value is only a toy name. That keeps the mechanics easy to see: a row becomes an NSItemProvider, the target list checks whether it can accept that type, then the controller updates the real arrays and reloads the UI.

The sample app tracks toys owned by three cats, with each cat showing its own table inside a larger collection view layout.

This article uses a starter app where multiple table views already exist. In that project, each collection-view cell contains one table view for one cat, and dragging a toy from one cell to another effectively reassigns the toy to a different cat.

Source Projects Starter template: StartTemplate. Completed version: DragAndDropNekoApp.
Animated example of dragging a row between different table views
The demo shows one toy leaving its original table and landing in another cat's list.

Enable drag support on the table view first, then make the view implement UITableViewDragDelegate.

The first UIKit step is small but required. Turn on drag interaction and assign the drag delegate to the object that owns the table, which in the source app is a collection-view cell subclass.

tableView.dragInteractionEnabled = true
tableView.dragDelegate = self

After that, conform to UITableViewDragDelegate and implement the method that begins a drag session:

extension CatCollectionViewCell: UITableViewDragDelegate {
    func tableView(
        _ tableView: UITableView,
        itemsForBeginning session: UIDragSession,
        at indexPath: IndexPath
    ) -> [UIDragItem] {
        // Build and return the dragged items here
    }
}

A drag begins with the row's actual value, encoded into Data and exposed through NSItemProvider.

In the tutorial, the payload is just the selected toy name. The row string becomes UTF-8 data, then that data is registered on an NSItemProvider as plain text.

let toyName = toys[indexPath.row]

guard let toyNameData = toyName.data(using: .utf8) else { return [] }

let provider = NSItemProvider()
provider.registerDataRepresentation(
    forTypeIdentifier: kUTTypePlainText as String,
    visibility: .all
) { completion in
    completion(toyNameData, nil)
    return nil
}

let item = UIDragItem(itemProvider: provider)

The article also stores the original cat name into the drag session so the drop side can later know where the item came from:

session.localContext = catName
return [item]

The destination table needs its own drop delegate before it can receive any of those dragged providers.

The next setup step mirrors the drag side. Assign a drop delegate to the table, then implement UITableViewDropDelegate.

tableView.dropDelegate = self
extension CatCollectionViewCell: UITableViewDropDelegate {
    func tableView(
        _ tableView: UITableView,
        performDropWith coordinator: UITableViewDropCoordinator
    ) {
        // Read and apply the dropped data here
    }
}

The core drop flow is: verify the type, load the dropped object, then read both the source cat and the target cat.

Because the dragged provider registered itself as plain text, the destination can check for that exact identifier before it tries to read anything.

if coordinator.session.hasItemsConforming(
    toTypeIdentifiers: [kUTTypePlainText as String]
) {
    coordinator.session.loadObjects(ofClass: NSString.self) { fetchedItems in
        guard let toyName = fetchedItems.first as? String else { return }

        if let originalCatName = coordinator.session.localDragSession?.localContext as? String {
            let targetCatName = self.catName
            // Move the toy from originalCatName to targetCatName
        }
    }
}

At that point the drop side knows all three pieces of information this article cares about: toyName, the original cat from localContext, and the destination cat from self.catName.

The real data change happens in the controller: remove the toy from one list, append it to the other, then reload.

This article keeps the actual data arrays in the view controller, so the table-owning cell should not mutate them directly. The article therefore routes the move through a delegate callback.

protocol DragAndDropActionDelegate: AnyObject {
    func moveToy(toyName: String, fromCat: String, toCat: String)
}
extension ViewController: DragAndDropActionDelegate {
    func moveToy(toyName: String, fromCat: String, toCat: String) {
        var fromCatToys = catToys[fromCat] ?? []
        fromCatToys.removeAll { toyNameInArray in
            toyNameInArray == toyName
        }
        catToys[fromCat] = fromCatToys

        var toCatToys = catToys[toCat] ?? []
        toCatToys.append(toyName)
        catToys[toCat] = toCatToys

        collectionView.reloadData()
    }
}

The collection-view cell only needs a weak delegate reference and one call when the drop finishes.

To connect the drop handler back to the controller-owned data model, the cell stores a weak delegate, the controller sets it during collection-cell creation, and the drop handler forwards the move request.

// CatCollectionViewCell.swift
weak var delegate: DragAndDropActionDelegate?
override func collectionView(
    _ collectionView: UICollectionView,
    cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
    ...
    cell.delegate = self
    ...
}
self.delegate?.moveToy(
    toyName: toyName,
    fromCat: originalCatName,
    toCat: self.catName
)

UITableView drag and drop becomes manageable once you separate three concerns: packaging data, accepting data, and mutating the real model.

That is the practical lesson from the original tutorial. The drag delegate should only describe what is being dragged, the drop delegate should only recover and validate what arrived, and the controller or shared model layer should stay responsible for the real data mutation.

With that split in place, moving rows between multiple table views becomes a straightforward UIKit workflow instead of a one-off special case.