Swift, UIKit: How can I diagnose EXC_BAD_INSTRUCTION in UIKit on tableView.performBatchUpdates()

84 Views Asked by At

I've been battling a UIKit issue with tableView.performBatchUpdates for many days.

I have a simple data model with 15 items driven by a tableView and tableViewCells with 15 corresponding cells (with recycling), one of which is a custom checkbox control. When clicking the checkbox ON, 7 of those 15 elements get deleted and added (in the middle), and then clicking it to OFF again, re-adds those same 7 cells back in (in the middle). The first 5 do not move. The last 3 get placed after the deleted/added 7 elements that are in the middle.

I diagnosed all the updates to the data source and to the tableview. The data source is appropriately updated first, and then deleteRows, insertRows and moveRow are called on tableView appropriately.

This works fine without scrolling the tableView (both to the ON and OFF state), causing my 7 rows to show and hide with a slide IN/OUT animation. I can repeat it many times and it will not crash until I do not scroll my cells out of view.

Now if I drag my tableView all the way up, to the point where most of the cells get cellForRow called on them (recycled and repopulated), and then use the checkbox, then I get a EXC_BAD_INSTRUCTION crash right on the tableView.performBatchUpdates() method (not related to any of my code, and stack trace just ends on performBatchUpdates()). I've done a bunch of research on this error, and usually it's a force unwrap of a NIL, as per: Diagnosing EXC_BAD_INSTRUCTION in Swift standard library

The issue is: my cells are quite complex with a lot of validation and configuration, and the stack trace doesn't lead to any of my code. I also verified that the Working (No scroll out of view first) vs Non working state (scroll out of view first), have no difference to the logic of deleteRows, addRows and moveRow inside performBatchUpdates(). Therefore it's something that subsequent calls on cellForRow() do to the cells that makes UIKit unhappy with their contents. And since the first time around everything works, it must be related to recycling of cells, and perhaps stale data.

Is there a way I can attach the UIKit source to my project so I can see what it's getting EXC_BAD_INSTRUCTION on?

Are there any other symbolic debugging tricks I can use to understand better which object(s) are related to the EXC_BAD_INSTRUCTION? If I make a sample project from scratch it's likely going to work fine, so it's specific to my actual cell structure and event logic that's upsetting UIKit.

Currently I'm forced not to use tableView animations (performBatchUpdates()), and simply do tableView.reloadData(), until I figure this out.

Images of what the exception looks like when caught with an Exception breakpoint: (the stack trace only goes up to performBatchUpdates() itself, and nothing within it or above it in UIKit). So the exception is in UIKit.

enter image description here

enter image description here

enter image description here

Here's the log of all Delete / Move / Add operations that are taking place:

Hide Fields: 
FORM DELETING: 0, 5
FORM DELETING: 0, 6
FORM DELETING: 0, 7
FORM DELETING: 0, 8
FORM DELETING: 0, 9
FORM DELETING: 0, 10
FORM DELETING: 0, 11
FORM MOVING: [0, 12], to: [0, 5]
FORM MOVING: [0, 14], to: [0, 7]
FORM MOVING: [0, 13], to: [0, 6]

Show Fields:
FORM ADDING: 0, 5
FORM ADDING: 0, 6
FORM ADDING: 0, 7
FORM ADDING: 0, 8
FORM ADDING: 0, 9
FORM ADDING: 0, 10
FORM ADDING: 0, 11
FORM MOVING: [0, 5], to: [0, 12]
FORM MOVING: [0, 7], to: [0, 14]
FORM MOVING: [0, 6], to: [0, 13]

// Elements 0-4 do not move.  They stay as the first 5 elements in the tableview 

This works on the first attempt for any number of iterations. This log remains the same, for the subsequent time when I first scroll the form completely out of view, and then retry to show/hide the fields. (All same logs, but crashes with EXC_BAD_INSTRUCTION). That's why i don't think it's related to the index arithmetic.

Here is the Delete, Add and Move logic inside performBatchUpdates that gets called after the data model is updated to reflect the new state:

tableView.deleteRows(at: toRemove, with: .none)
tableView.insertRows(at: toAdd, with: .none)

for (key, value) in toMove {
    print("FORM MOVING: \(key), to: \(value)")
    tableView.moveRow(at: key, to: value)
}
// toRemove, toAdd and toMove are populated with indexes you see above. 
1

There are 1 best solutions below

0
FranticRock On

After digging in deeper into all the logic that rendered my cells in cellForRow(atIndexPath...), I found that one of the cells was triggering a call-chain that resulted in Underlying data model updates without reloading the tableview. (it was replacing one of the items in the data source at the same time as the tableview was already being reloaded - unintentionally).

This explained that only after this undesired out-of-sync update to the underlying data source, I would have a crash when performing the next performBatchUpdates operation . (or even if i did: tableView.beginUpdates(), tableView.endUpdates() <- that would crash as well with the same EXC_BAD_INSTRUCTION).

Therefore an out-of-sync data source before calling performBatchUpdates OR .beginUpdates()/.endUpdates() was the problem. As soon as I removed the unintended change to the data source, performBatchUpdates runs and animates fine - as long as in it's block the data source update matches the add/insert/move actions on the tableview too.

It took me a very long time to find because of the event chain complexity in cellForRow(...)