UITableView
assertion failure
iOS development
cell animations
debugging

Assertion failure in -UITableView _endCellAnimationsWithContext

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

This UITableView assertion usually means UIKit finished an animated update and found that the row counts no longer match the data source. The method name looks private and mysterious, but the bug is usually simple: the model and the table view were updated in different ways or in the wrong order.

Why the Assertion Appears

UITableView keeps strict accounting during inserts, deletes, moves, and reloads. After an update block completes, the table view asks the data source how many sections and rows now exist. Those answers must be consistent with the operations you just requested.

If your array has three items and you call insertRows without appending the new item first, the table view expects four rows while the data source still reports three. UIKit detects the mismatch and raises an assertion.

The same thing happens in reverse during deletion. If the table view is told to delete a row while the backing model still contains it, the internal counts do not line up.

Update the Model First

The safest pattern is:

  1. mutate the backing data
  2. issue the matching table view operation immediately
  3. ensure the data source methods reflect the new state

Here is a correct insertion example:

swift
1import UIKit
2
3final class ViewController: UIViewController, UITableViewDataSource {
4    @IBOutlet private weak var tableView: UITableView!
5    private var items = ["A", "B", "C"]
6
7    @IBAction private func addItem() {
8        let row = items.count
9        items.append("D")
10
11        tableView.performBatchUpdates {
12            tableView.insertRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
13        }
14    }
15
16    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
17        items.count
18    }
19}

The critical line is items.append("D") before the insert animation. By the time UITableView validates its state, the data source already reports the new row count.

Apply the Same Rule to Deletions

Deletion should mirror insertion:

swift
1func removeItem(at row: Int) {
2    items.remove(at: row)
3
4    tableView.performBatchUpdates {
5        tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
6    }
7}

Do not delete visually first and mutate the model later. That is one of the fastest paths to this assertion.

For section operations, the same rule applies. Update the structure backing numberOfSections and numberOfRowsInSection before issuing insertSections, deleteSections, or moveSection.

Avoid Stale Index Paths and Conflicting Operations

Many crashes happen when index paths are cached before the model changes and reused afterward. Once you insert or remove an element, later row positions may no longer mean what they meant a moment earlier.

Another common problem is asking for conflicting animations in the same cycle, such as reloading and deleting the same row. UIKit cannot reconcile a contradictory instruction set.

When the change set becomes complicated, build the final model state first and derive a coherent update plan from it. If that is still messy, a plain reloadData() is better than a broken animated diff.

Stay on the Main Thread

UITableView is UIKit, and UIKit belongs on the main thread. If network callbacks or background work mutate the model and call row APIs off the main queue, you can create both race conditions and impossible table states.

swift
1DispatchQueue.main.async {
2    self.items.append("New item")
3    self.tableView.insertRows(
4        at: [IndexPath(row: self.items.count - 1, section: 0)],
5        with: .automatic
6    )
7}

This does not fix bad bookkeeping, but it removes a whole class of threading-related failures.

Prefer Safer Abstractions When Possible

If you are targeting modern iOS, diffable data sources are often the best long-term fix. Instead of manually keeping arrays and row operations synchronized, you apply a snapshot and let UIKit compute the changes. That dramatically reduces mismatch bugs.

If a screen is simple, reloadData() may also be perfectly reasonable. Animated fine-grained updates are only worth the risk when the extra UI polish matters.

Common Pitfalls

  • Calling insertRows or deleteRows before updating the backing collection.
  • Returning stale counts from numberOfRowsInSection during an animation.
  • Reusing index paths that were captured before the model changed.
  • Mixing incompatible operations such as reloading and deleting the same row.
  • Performing table view updates on a background thread.

Summary

  • This assertion means UITableView's animated update does not match the data source state.
  • Update the model first, then issue the corresponding row or section operation.
  • Keep row counts and section counts consistent throughout the update.
  • Avoid stale index paths and conflicting operations.
  • Use diffable data sources or reloadData() when manual animated updates become hard to reason about.

Course illustration
Course illustration

All Rights Reserved.