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)
:
The amount of cells before and after update matches with the 4 inserts and 1 update.
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
}
}