Drag to Reorder in UITableView with Diffable Datasource
This post is brought to you by Emerge Tools, the best way to build on mobile.
let concatenatedThoughts = """
Welcome to Snips! Here, you'll find a short, fully code complete sample with two parts. The first is the entire code sample which you can copy and paste right into Xcode. The second is a step by step explanation. Enjoy!
"""
The Scenario
Reorder rows by dragging on them in a table view using diffable datasource.
import UIKit
struct VideoGame: Hashable {
let id = UUID()
let name: String
}
extension VideoGame {
static var data = [VideoGame(name: "Mass Effect"),
VideoGame(name: "Mass Effect 2"),
VideoGame(name: "Mass Effect 3"),
VideoGame(name: "ME: Andromeda"),
VideoGame(name: "ME: Remaster")]
}
class TableDataSource: UITableViewDiffableDataSource<Int, VideoGame> {
// 1
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
// 1 continued
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard let fromGame = itemIdentifier(for: sourceIndexPath),
sourceIndexPath != destinationIndexPath else { return }
var snap = snapshot()
snap.deleteItems([fromGame])
if let toGame = itemIdentifier(for: destinationIndexPath) {
let isAfter = destinationIndexPath.row > sourceIndexPath.row
if isAfter {
snap.insertItems([fromGame], afterItem: toGame)
} else {
snap.insertItems([fromGame], beforeItem: toGame)
}
} else {
snap.appendItems([fromGame], toSection: sourceIndexPath.section)
}
apply(snap, animatingDifferences: false)
}
}
class DragDropTableViewController: UIViewController {
var videogames: [VideoGame] = VideoGame.data
let tableView = UITableView(frame: .zero, style: .insetGrouped)
lazy var datasource: TableDataSource = {
let datasource = TableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, model) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = model.name
return cell
})
return datasource
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// 2
tableView.dragDelegate = self
tableView.dropDelegate = self
tableView.dragInteractionEnabled = true
var snapshot = datasource.snapshot()
snapshot.appendSections([0])
snapshot.appendItems(videogames, toSection: 0)
datasource.applySnapshotUsingReloadData(snapshot)
}
}
// 3
extension DragDropTableViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = datasource.itemIdentifier(for: indexPath) else {
return []
}
let itemProvider = NSItemProvider(object: item.id.uuidString as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
// 4
extension DragDropTableViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
// If you don't use diffable data source, you'll need to reconcile your local data store here.
// In our case, we do so in the diffable datasource subclass.
}
}
Now, you can drag and reorder the rows:
The Breakdown
Step 1
Diffable datasource plays an important role with reordering. You’ll need to override two functions:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
and
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
The first is easy as you’ll likely just return true. The second one is a bit more involved and represents the majority of the work. Simply put, this is where you reconcile your data in the diffable datasource and apply the diff.
Step 2
The tableview instance will need a drag and drop delegate set. Also, opt in to drag and drop on iPhone by setting dragInteractionEnabled
.
Step 3
In the UITableViewDragDelegate
there is one required function. Here, you’ll create a NSItemProvider
to hand off to a corresponding UIDragItem
. Return an array of drag items here as that’s what the system will vend when a drop occurs.
Step 4
Finally, in the UITableViewDropDelegate
there are two required functions. In tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal
, return
a UITableViewDropProposal
with a .move
operation and .insertAtDestinationIndexPath
as the intent. In our sample, this means all rows will reorder. If you want something different, this is where you’d change behavior. The last function is required,
and if you weren’t using a diffable datasource - this is where you’d reconcile your local data and apply those changes.
Until next time ✌️