Post not yet marked as solved
Post marked as unsolved with 0 replies, 70 views
Hello,
I'm experiencing an issue with UITableViewDragDelegate and UITableViewDropDelegate where a random cell within the UITableView will disappear when dragging a cell around. I believe I'm either missing a step with these protocols, or this is a bug within UIKit. Any help would be greatly appreciated! I'm on Xcode 13.4 using an iOS 15.5 simulator.
I've spun up some sample code that can easily reproduce one variant of the issue. This repro involves the "autoscroll" feature provided by these protocols, where the table will automatically scroll when a dragged cell approaches the top or bottom edge of the table. It also seems to happen more frequently when tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) sometimes returns a UITableViewDropProposal with the move operation (for moves that are permitted) and other times returns a UITableViewDropProposal with the forbidden operation.
This isn't the only time I've seen a cell disappearing with these drag & drop protocols, but it's the easiest to reproduce. When this bug does occur, you can use Xcode’s Debug View Hierarchy tool to inspect the cell that disappeared. Each time this happens, the cell is still within the view hierarchy, but its alpha has been set to 0.
I have a ticket within Feedback Assistant (FB10449257) but I'm also posting here in case someone else has run into this issue. To reproduce the issue:
Start by dragging a cell to the bottom of the table.
Once the table scrolls to the bottom, drag the cell to the very bottom edge of the table's frame. This will cause the forbidden operation to be returned from tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) because destinationIndexPath is nil.
Drag the cell to the top edge of the table to scroll back up.
A cell will be missing.
Here's my sample code:
import UIKit
class ViewController: UIViewController {
lazy var tableView: UITableView = {
let table = UITableView(frame: view.bounds)
table.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(table)
table.register(UITableViewCell.self, forCellReuseIdentifier: "DefaultCell")
return table
}()
lazy var dataSource: UITableViewDiffableDataSource<Int, Int> = {
.init(tableView: tableView) { tableView, indexPath, itemIdentifier in
let cell = tableView.dequeueReusableCell(withIdentifier: "DefaultCell", for: indexPath)
var configuration = cell.defaultContentConfiguration()
configuration.text = "Item number \(itemIdentifier)"
cell.contentConfiguration = configuration
return cell
}
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
tableView.dragDelegate = self
tableView.dropDelegate = self
var snapshot = dataSource.snapshot()
snapshot.appendSections([0])
snapshot.appendItems(Array(0...25))
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension ViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let dragItem = UIDragItem(itemProvider: .init())
dragItem.localObject = dataSource.itemIdentifier(for:indexPath)!
return [dragItem]
}
func tableView(_ tableView: UITableView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool {
true
}
}
extension ViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard destinationIndexPath != nil else {
print("destinationIndexPath is nil")
return UITableViewDropProposal(operation: .forbidden)
}
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
// This logic is missing safety checks and is just for demo purposes.
var snapshot = dataSource.snapshot()
let destination = coordinator.destinationIndexPath!
let dragItem = coordinator.items.first!.dragItem
snapshot.moveItem(dragItem.localObject as! Int, afterItem: dataSource.itemIdentifier(for: destination)!)
dataSource.apply(snapshot)
coordinator.drop(dragItem, toRowAt: destination)
}
}
Thank you!