3

I am animating a UITable with a toggle effect. To be able to reach that I am using a delta like this:

fileprivate func onDataUpdatedClosure() {
    deltaProcessingQueue.async {
        let oldList = self.rows
        let newList = self.getCells()

        let deltaAdded = newList.filter { !oldList.contains($0) }.compactMap { newList.firstIndex(of: $0) }
        let deltaRemoved = oldList.filter { !newList.contains($0) }.compactMap { oldList.firstIndex(of: $0) }
        let deltaReload = deltaAdded.filter { deltaRemoved.contains($0) }

        let mapRowsAdded = deltaAdded.filter { !deltaReload.contains($0) }
            .map { DeltaOvm.insert(path: IndexPath(item: $0, section: 0)) }

        let mapRowsRemoved = deltaRemoved.filter { !deltaReload.contains($0) }
            .map { DeltaOvm.delete(path: IndexPath(item: $0, section: 0)) }

        let mapRowsReloaded = deltaReload.map { DeltaOvm.update(path: IndexPath(item: $0, section: 0)) }

        let delta: [DeltaOvm] = mapRowsAdded + mapRowsReloaded + mapRowsRemoved

        if !delta.isEmpty {
            DispatchQueue.main.async { [weak self] in
                self?.rows = newList
                self?.onDataUpdated?(delta)
            }
        }
    }
}

which gets the difference between how table looked before the update and after. It all works fine, until I get to insert 4 cells on one of the objects. Exception happens with the following message:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'Invalid update: invalid number of rows in section 0. The number of rows 
contained in an existing section after the update (9) must be equal to the number 
of rows contained in that section before the update (5), plus or minus the number 
of rows inserted or deleted from that section (4 inserted, 1 deleted) and plus or 
minus the number of rows moved into or out of that section (0 moved in, 0 moved out).

What's strange is that I'm not deleting any rows, as here you can see the delta content at the breakpoint just at the self?.onDataUpdated?(delta):

enter image description here

The amount of cells before and after update matches with the 4 inserts and 1 update. enter image description here The last update delta cell is causing it. What's interesting is that if I make a number of cells smaller - this works just fine. It starts to fail if there are more that 4 cells being added.

Here is the DeltaOvm enum for reference:

enum DeltaOvm: Equatable {
case insert(path: IndexPath)
case update(path: IndexPath)
case delete(path: IndexPath)

static func == (lhs: DeltaOvm, rhs: DeltaOvm) -> Bool {
    switch (lhs, rhs) {
    case let (.delete(index1), .delete(index2)):
        return index1 == index2
    case let (.update(index1), .update(index2)):
        return index1 == index2
    case let (.insert(index1), .insert(index2)):
        return index1 == index2
    default: return false
    }
  }
}

This is the way table is being updated in the view controller:

fileprivate func setupOnDataUpdated() {
    viewModel.onDataUpdated = { [weak self] (deltas) in

        var insertIndexes = [IndexPath]()
        var updateIndexes = [IndexPath]()
        var deleteIndexes = [IndexPath]()

        for deltaItem in deltas {
            switch deltaItem {
            case .insert(let index):
                insertIndexes.append(index)
            case .update(let index):
                updateIndexes.append(index)
            case .delete(let index):
                deleteIndexes.append(index)
            }
        }

        self?.tableView.performBatchUpdates({
            self?.tableView.insertRows(at: insertIndexes, with: .top)
            self?.tableView.reloadRows(at: updateIndexes, with: .none)
            self?.tableView.deleteRows(at: deleteIndexes, with: .bottom)
        }, completion: nil)
    }
}

This is the row:

enum RowViewModel: Equatable {
    case collectedCellParent(CollectedCellParentViewModel)
    case collectedCellChild(CollectedCellChildViewModel)

    func associatedValue() -> Any {
        switch self {
            case .collectedCellParent(let value): return value
            case .collectedCellChild(let value): return value
        }
    }
}

View model of the cell in question:

class CollectedCellParentViewModel: Equatable, ViewModelPressible {

    var title: String
    var imageName: String
    var cellPosition: CellPosition
    var cellPressed: (() -> Void)?

    init(title: String,
         imageName: String,
         cellPosition: CellPosition,
         cellPressed: (() -> Void)?) {

        self.title = title
        self.imageName = imageName
        self.cellPosition = cellPosition
        self.cellPressed = cellPressed
    }

    static func == (lhs: CollectedCellParentViewModel, rhs: CollectedCellParentViewModel) -> Bool {
        return lhs.title == rhs.title &&
               lhs.cellPosition == rhs.cellPosition
        }
    }
Async-
  • 3,140
  • 4
  • 27
  • 49
  • What is `rows` and how are you actually updating the table? – Sulthan Mar 17 '22 at 09:51
  • I edited a question to add more code @Sulthan – Async- Mar 17 '22 at 10:39
  • Generally speaking, are you sure that you are inserting/updating/deleting correct indices? If you first insert some indices and then you ask the table to reload others, you must make sure that the indices to reload are correctly updated after the insertion. – Sulthan Mar 17 '22 at 16:44
  • yes, 100% indices are correct. update one is existing one – Async- Mar 18 '22 at 08:38
  • I encountered this error whenever I tried to call code `insert, delete` code like `self?.tableView.insertRows(at: insertIndexes, with: .top)` **before** I updated the `datasource` so I am not sure if you update your datasource before or after triggering this ? – Shawn Frank Mar 27 '22 at 09:47
  • as you can see along these lines I do: ```self?.rows = newList self?.onDataUpdated?(delta)``` – Async- Mar 28 '22 at 12:36
  • Maybe [this post](https://medium.com/bumble-tech/batch-updates-for-uitableview-and-uicollectionview-baaa1e6a66b5) helps you? It says _Reloads can not be used in conjunction with other changes, as under some circumstances they lead to memory corruption issues with internal UIKit entities. This has been worked around by asking a corresponding data source to update a specified cell in a way it’s reloaded (updated) when reused._ – Reinhard Männer Mar 28 '22 at 18:55
  • 1
    You deltaReload computation is wrong. In fact, I don't see how you can even determine if a change was made to an item in the list. What is the criteria for a change? – Rob C Mar 30 '22 at 06:06
  • @RobC it's all about expanding/collapsing cell (parent > children) - when user taps it - first data model is updated (rows) and then a delta is calculated. I know the issue now: the indexes of ```delete, reload``` have to be of the collection prior to update, and of the ```insert``` - after the update. Which is not the case with this delta, but let me refactor that and edit the post. – Async- Mar 30 '22 at 09:16
  • 1
    Related question: https://stackoverflow.com/questions/33186659/drop-down-list-in-uitableview-in-ios – Cristik Mar 30 '22 at 10:17

1 Answers1

0

I had a similar issue with UICollectionView and solved it by updating the data source within the performBatchUpdates' update block.

From the UICollectionView.performBatchUpdates(_:completion:) method documentation:

If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).

I would suggest doing the same with the UITableView. The documentation does not explicitly say it but the updates block documentation from UITableView.performBatchUpdates(_:completion:) could indicate it might be true:

The block that performs the relevant insert, delete, reload, or move operations. In addition to modifying the table's rows, update your table's data source to reflect your changes.

reggian
  • 627
  • 7
  • 21
  • tried that, it didn't work, same crash. Also tried the beginUpdates() endUpdates() and also same crash. – Async- Mar 29 '22 at 08:52