Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Adopting drag and drop in a table view

At a glance

Item Summary
Purpose Supports local row reordering and external plain-text insertion through table-view drag and drop.
App architecture TableViewController owns one mutable Model; feature extensions divide standard table data, drag production, and drop consumption.
Main patterns Small MVC, framework delegates, read-only model projection, transfer object, and extension-based decomposition.
Project style One UIKit target grouped into Model/ and TableViewController/, with +Dragging, +Data, +Drag, and +Drop extensions.

Project structure

TableViewDragAndDrop/
├── AppDelegate.swift
├── SceneDelegate.swift
├── Model/
│   ├── Model.swift
│   └── Model+Dragging.swift
└── TableViewController/
    ├── TableViewController.swift
    ├── TableViewController+Data.swift
    ├── TableViewController+Drag.swift
    └── TableViewController+Drop.swift

Structure observations

  • Core model mutation is separate from drag representation, but both remain extensions of the same value owner.
  • Controller extensions align one-to-one with UIKit data-source/delegate boundaries.

Overall architecture

Reference code

TableViewDragAndDrop/TableViewController/TableViewController.swift:17 — the controller owns the model and installs itself at both framework boundaries.

class TableViewController: UITableViewController {
    var model = Model()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dragInteractionEnabled = true
        tableView.dragDelegate = self
        tableView.dropDelegate = self
        navigationItem.rightBarButtonItem = editButtonItem
    }
}

Interpretation

Local moves reuse the table’s traditional moveRowAt path, while external drops decode provider data and insert rows. Both paths commit through Model, keeping the array authoritative.

Ownership and state

Ownership evidence

TableViewDragAndDrop/Model/Model.swift:11 — callers can read place names, but array writes are restricted to model methods.

struct Model {
    private(set) var placeNames = ["Yosemite", "Yellowstone", "Theodore Roosevelt"]

    mutating func moveItem(at sourceIndex: Int, to destinationIndex: Int) {
        guard sourceIndex != destinationIndex else { return }
        let place = placeNames.remove(at: sourceIndex)
        placeNames.insert(place, at: destinationIndex)
    }
}
Owner Object or state Relationship Mutation authority
Table controller Model value Creates and stores Controller extensions call model commands
Model Place-name array Exclusive stored value Model methods because setter is private
Drag item Item provider Creates/retains during drag Provider registration closure supplies data
UITableView Drag/drop delegate links Framework stores callback references UIKit invokes controller

Class and protocol design

Type or protocol Responsibility Depends on
Model Expose rows and perform move/add mutations [String]
Model dragging extension Validate incoming types and serialize one row UIDropSession, NSItemProvider
TableViewController data extension Render rows and commit traditional local moves UITableViewDataSource/Delegate
Drag delegate extension Request model-produced drag items and manage edit-button availability UITableViewDragDelegate
Drop delegate extension Choose cancel/move/copy and decode external text UITableViewDropDelegate

No app-defined protocol layer is present; concrete extensions directly implement UIKit’s boundaries.

Access control

Symbol Access Verified effect Likely rationale
Model.placeNames private(set) All target code can read rows, but only Model and its same-file scope can assign/mutate storage. Preserve mutation commands as the write boundary.
model implicit internal Controller extensions in separate files can read and mutate it. Required by the chosen conformance-per-file organization.
Model commands/drag helpers implicit internal Same target can use them. App-only feature API.
Controller type implicit internal Storyboard can instantiate inside the target. No library surface.

Reference code

TableViewDragAndDrop/Model/Model+Dragging.swift:24 — transfer construction is exposed as a model-facing helper, while raw storage stays protected.

func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
    let placeName = placeNames[indexPath.row]
    let data = placeName.data(using: .utf8)
    let itemProvider = NSItemProvider()
    itemProvider.registerDataRepresentation(
        forTypeIdentifier: kUTTypePlainText as String,
        visibility: .all) { completion in
            completion(data, nil)
            return nil
        }
    return [UIDragItem(itemProvider: itemProvider)]
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Row storage and mutation Model.swift Establishes one data invariant.
Drag serialization/type support Model+Dragging.swift Translation at the model/transfer boundary.
Cell rendering/local row move TableViewController+Data.swift Standard table data lifecycle.
Drag session production/UI availability TableViewController+Drag.swift Drag delegate concern.
Proposal and external insertion TableViewController+Drop.swift Drop delegate concern.

Design patterns

Pattern Source evidence Purpose or tradeoff
MVC TableViewDragAndDrop/TableViewController/TableViewController.swift:10 Controller mediates table callbacks over an independent model value.
Encapsulated mutation TableViewDragAndDrop/Model/Model.swift:12 private(set) prevents arbitrary array writes.
Framework delegates TableViewDragAndDrop/TableViewController/TableViewController+Drop.swift:10 UIKit asks the owner for policy and delivery handling.
Transfer object TableViewDragAndDrop/Model/Model+Dragging.swift:28 Plain-text provider supports cross-app delivery.
Extension decomposition TableViewDragAndDrop/TableViewController/TableViewController+Data.swift:11 Organizes callbacks while retaining a single controller owner.

Naming conventions

  • Model is intentionally generic because the sample domain is incidental.
  • moveItem and addItem name model mutations; dragItems names representation production.
  • +Data, +Drag, +Drop, and +Dragging filenames expose boundary roles.
  • UIKit delegate signatures retain framework vocabulary such as proposal, coordinator, and destination index path.

Architecture takeaways

  • Keep table rows authoritative in the model and route both local and external operations through it.
  • Use private(set) when UI needs read access but should not mutate collection storage directly.
  • Separate a local move from an external copy because their data-delivery paths differ.
  • Organize large delegate surfaces by extension without inventing extra owners.

Source map

Source file Relevant symbols
TableViewDragAndDrop/Model/Model.swift Row storage and mutation commands
TableViewDragAndDrop/Model/Model+Dragging.swift Transfer validation/serialization
TableViewDragAndDrop/TableViewController/TableViewController.swift Model ownership and delegate setup
TableViewDragAndDrop/TableViewController/TableViewController+Data.swift Cell data and local move
TableViewDragAndDrop/TableViewController/TableViewController+Drag.swift Drag production
TableViewDragAndDrop/TableViewController/TableViewController+Drop.swift Drop proposal and external insertion