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
flowchart LR
Controller["TableViewController"] --> Model["Model"]
Controller ..-> Data["UITableView data callbacks"]
Controller ..-> Drag["UITableViewDragDelegate"]
Controller ..-> Drop["UITableViewDropDelegate"]
Data --> Model
Drag --> Model
Model --> Provider["NSItemProvider plain text"]
Provider --> Drop
Drop -->|external copy| Model
Data -->|local move| Model
Model --> Table["UITableView rows"]
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
classDiagram
TableViewController *-- Model : owns value
Model *-- Array~String~ : owns placeNames
UITableView o-- UITableViewDragDelegate : nonowning callback
UITableView o-- UITableViewDropDelegate : nonowning callback
UIDragItem *-- NSItemProvider : owns
UIDropSession o-- UIDragItem : supplies
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 |