Search code examples
iosswiftrealm

How do I avoid delay or adjust timing of coordinator.drop when using a Swift Realm Results<> or List<> instance?


I am using Realm Swift to build a todo list while learning iOS development. I'm running into a problem with my collectionView that happens when I try to re-order items using coordinator.drop(item.dragItem, toItemAt: destinationIndexPath).

When I include the coordinator the dropped item first displays the contents of the cell that was originally at that index and then after a delay is replaced by the contents of the cell that was dragged. If I don't include the coordinator then the drag and drop works as intended and the item is correctly swapped when dropped (but the animation is that of a cell shrinking down to the center and a bit jarring).

When I reoder items in the collectionView:

func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
        if let item = coordinator.items.first,
            let sourceIndexPath = item.sourceIndexPath {
                do {
                    try self.realm.write {
                          // standard category is of type Category
                          // items is of type List<Task>
                          standardCategory?.items.remove(at: sourceIndexPath.item)
                          standardCategory?.items.insert(item.dragItem.localObject as! Task, at: destinationIndexPath.item)
                    }
                } catch {
                    print("error reording new items, \(error)")
                }
            coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
        }
    }

Inside of viewDidLoad() to handle updating the collectionView:

 self.notificationToken = tasks!.observe {
            (changes: RealmCollectionChange) in
            switch changes {
            case .initial(_):
                self.collectionView.reloadData()
            case .update(_, let deletions, let insertions, let modifications):
                 self.collectionView.performBatchUpdates({
                    self.collectionView.deleteItems(at: deletions.map({ IndexPath(item: $0, section: 0) }))
                    self.collectionView.insertItems(at: insertions.map({ IndexPath(item: $0, section: 0) }))
                    self.collectionView.reloadItems(at: modifications.map({ IndexPath(item: $0, section: 0) }))
                 }, completion: { status in
                    self.collectionView.reloadData()
                 })
            case .error(_):
                print("error")
        }

Solution

  • I think I have it working now. Assuming edge cases don't break it here's the answer:

    I needed to handle the model updates and collectionView updates outside of tasks.observe so that I could control the order of events more exactly. You can do that by using try realm.commitWrite(withoutNotifying: [notificationToken!])

    The key I think was to reorder the sequence of deleting and updating the items to: 1. delete from model 2. remove from collection view 3. inset into model 4. use coordinator to drop item 5. insert into collection view 6. reload section (optional I think, I needed it for UI changes)

    func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
            if let item = coordinator.items.first {
                if let localObject = item.dragItem.localObject as? Task {
                    if let sourceIndexPath = item.sourceIndexPath {
                        do {
                            try self.realm.write {
                                standardCategory?.items.remove(at: sourceIndexPath.item)
                                try realm.commitWrite(withoutNotifying: [notificationToken!])
                            }
                        } catch { print("error reording new items, \(error)") }
                        collectionView.deleteItems(at: [sourceIndexPath])
                    }
                    do {
                        try self.realm.write {
                            standardCategory?.items.insert(localObject, at: destinationIndexPath.item)
                            try realm.commitWrite(withoutNotifying: [notificationToken!])
                        }
                    } catch { print("error reording new items, \(error)") }
    
                    self.collectionView.performBatchUpdates({
                        coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
                        collectionView.insertItems(at: [destinationIndexPath])
                    }, completion: { status in
                        let indexSet = IndexSet(integer: destinationIndexPath.section)
                        collectionView.reloadSections(indexSet)
                    })
                }
            }
        }
    

    SOLUTION: It turns out you can't use Results when sorting using drag and drop (not 100% sure why but that would be nice to know). Here's the current working solution using List instead:

    func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
            if let item = coordinator.items.first {
                if let localObject = item.dragItem.localObject as? Task {
                    if let sourceIndexPath = item.sourceIndexPath {
                        self.collectionView.performBatchUpdates({
                            do {
                                standardCategory!.realm!.beginWrite()
    
                                standardCategory!.items.remove(at: sourceIndexPath.item)
                                collectionView.deleteItems(at: [sourceIndexPath])
    
                                self.standardCategory!.items.insert(localObject as Task, at: destinationIndexPath.item)
    
                                coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
    
                                collectionView.insertItems(at: [destinationIndexPath])
                                try standardCategory!.realm!.commitWrite(withoutNotifying: [notificationToken!])
    
                            } catch { print("error reording new items, \(error)") }
                        }, completion: nil)
                    }
                }
            }
        }