[SC]()

iOS. Apple. Indies. Plus Things.

Drag to Reorder in UITableView with Diffable Datasource

// Written by Jordan Morgan // Jul 26th, 2021 // Read it in about 3 minutes // RE: Snips

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:

Demo of drag and drop to reorder a UITableView.

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 ✌️

···

Spot an issue, anything to add?

Reach Out.