Pedro Paulo Amorim
Pedro Paulo Amorim

Reputation: 1949

Invalid update on TableView

Hi friends of StackOverflow.

I have a chat screen on my app and I it perform a insertion and deletion based on the actual size of the an Array. Look this:

func addObject(object: Object?) {

    if comments == nil || object == nil || object?.something == nil || object?.anything == nil {
      return
    }

    self.objectsTableView.beginUpdates()

    if self.objects!.count == 10 {
      self.objects?.removeAtIndex(9)
      self.objectsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow : 9, inSection: 0)], withRowAnimation: .Right)
    }

    self.objects?.insert(object!, atIndex: 0)
    self.objectsTableView.insertRowsAtIndexPaths([NSIndexPath(forRow : 0, inSection: 0)], withRowAnimation: .Right)

    self.objectsTableView.endUpdates()

  }

But after some stress test, the log notify:

Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).

I don't know whats happening, this happens only when the insert of objects is very extreme, like one per 0.2 seconds.

Someone know that I can do?

Upvotes: 3

Views: 2994

Answers (2)

Pedro Paulo Amorim
Pedro Paulo Amorim

Reputation: 1949

Name of the guy that solved the problem: Semaphore

The error still happens, but only with a high size of items on list. I don't know what can be.

The DataSource protocol:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let count = self.objects?.count ?? 0
    if self.semaphore != nil && semaphoreCode == BLOCKED_STATE {
      dispatch_semaphore_signal(self.semaphore!)
    }
    return count
  }

The method that add object:

func addObject(object: Object?) {

    if object == nil {
      return
    }

    if self.semaphore != nil {
      let tempSemaCode = dispatch_semaphore_wait(semaphore!, 100000)
      if tempSemaCode == BLOCKED_STATE {
        self.semaphoreCode = RELEASED_STATE
      }
    }

    if self.objects != nil && semaphoreCode != BLOCKED_STATE {

      var needsRemoveLastItem = false
      if self.objects!.count == 10 {
        self.objects?.removeAtIndex(9)
        needsRemoveLastItem = true
      }
      self.objects?.insert(object!, atIndex: 0)

      if self.objects!.count > 0 {
        self.objectsTableView.beginUpdates()
        if needsRemoveLastItem {
          self.objectsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow : 9, inSection: 0)], withRowAnimation: .Right)
        }
        self.objectsTableView.insertRowsAtIndexPaths([NSIndexPath(forRow : 0, inSection: 0)], withRowAnimation: .Right)
        self.objectsTableView.endUpdates()
        self.semaphore = dispatch_semaphore_create(BLOCKED_STATE)
      }

    }

  }

Upvotes: 0

SwiftArchitect
SwiftArchitect

Reputation: 48552

Model mismatch

The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted)

In plain English for the reasonable man, the UITableView thinks you should have 11 rows:
10 before the update + 1 inserted.

number of rows contained in an existing section after the update (1)

...refers to numberOfRowsInSection is returning 1 for section 0, which indicates that the objects array is out of sync, assuming you use something like below:

override func tableView(tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int {
    return objects.count
}

Use NSFetchedResultsController

A clean solution is to use NSFetchedResultsController to be the interface between your model and the UI. It has well studied boilerplate code and is a great platform to ensure thread safety. Documentation here.


Note:

Neat effect! The cell seems to rotate around to the top.
I could not break it using the Gist you produced, nor scheduling multiple concurrent tests. There must be a rogue access to your Object array.

Demo

This simplified version works. Just hook doPlusAction to a button action and watch it loop:

neat effect

class TableViewController: UITableViewController {
    var objects:[Int] = [0,1,2,3,4]
    var insertions = 5

    @IBAction func doPlusAction(sender: AnyObject) {
        tableView.beginUpdates()

        objects.removeAtIndex(4)
        tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Right)
        objects.insert(insertions++, atIndex: 0)
        tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Right)
        tableView.endUpdates()

        let delay = 0.1 * Double(NSEC_PER_SEC) //happens the same with this too, when reach 100-150 items
        let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
        dispatch_after(time, dispatch_get_main_queue()) { () -> Void in
            self.doPlusAction(self)
        }
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return objects.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
        cell.textLabel!.text = "Cell \(objects[indexPath.row])"
        return cell
    }
}

Upvotes: 1

Related Questions