UITableViewDragDelegate & UITableViewDropDelegate occasionally set cell alpha to 0

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:

  1. Start by dragging a cell to the bottom of the table.
  2. 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.
  3. Drag the cell to the top edge of the table to scroll back up.
  4. 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!

Posting an update for anyone that is experiencing this same issue.

I opened a technical support incident with Apple and was informed that the cells don’t exhibit the disappearing bug if you use an .insertIntoDestinationIndexPath intent instead of .insertAtDestinationIndexPath. (Thank you for that pointer!) This could be a UITableView bug related to the cells shifting out of the way for insertAt. I'll know more when I receive a response in Feedback Assistant.

Also, our team has not been able to reproduce this issue with the equivalent UICollectionView APIs. If the insertAt functionality is needed, this may be a case where we must refactor the UI from UITableView to UICollectionView.

UITableViewDragDelegate & UITableViewDropDelegate occasionally set cell alpha to 0
 
 
Q