iosswiftuicollectionviewuicontextmenuinteraction

Weird animation when deleting item from UICollectionView while UIContextMenu is shown


I'm using UIContextMenuInteraction to show a context menu for UICollectionView as follows:

func collectiovnView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in
        let deleteAction = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in
            self.deleteItem(at: indexPath)
        }
        return UIMenu(title: "Actions", children: [deleteAction])
    })
}

func deleteItem(at indexPath: IndexPath) {
    self.collectionView.performBatchUpdates({
        self.items.remove(at: indexPath.item)
        self.collectionView.deleteItems(at: [indexPath])
    })
}

Everything works well, but when I tap the "Delete" item, a weird animation happens where the deleted item stays in its place while other items are moving, and then it disappears instantly. And sometimes I even see an empty space or a random item for fraction of a second before the new item appears.

If I call collectionView.deleteItems() while the context menu isn't shown the deletion animation works as expected.


Solution

  • It looks like the weird animation is a result of a conflict between two animations that run at almost the same time:

    1. Deletion animation: When "Delete" item is tapped, collectionView.deleteItems() is called and the specified collection item is deleted with animation.
    2. Menu dismiss animation: After the menu item is tapped, the context menu is also dismissed with another animation that shows the deleted item for a fraction of a second.

    This looks like a bug that should be fixed by Apple. But as a workaround, I had to delay the deletion until the dismiss animation completed:

    func deleteItem(at indexPath: IndexPath) {
        let delay = 0.4 // Seconds
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            self.collectionView.performBatchUpdates({
                self.items.remove(at: indexPath.item)
                self.collectionView.deleteItems(at: [indexPath])
            })
        }
    }
    

    The 0.4 seconds is the shortest delay that worked for me.