Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableView Freezes After Swipe to Delete But Not Whole UI

Full-screen table view iPad-only app. I have enabled swipe to delete on my rows. The row animation always finishes after the delete (commitEditingStyle completes), but occasionally the entire table view freezes. Not the whole UI, mind you, so it's not a blocked main thread. I am able to tap a column header or tap the back button on the navigation controller, but the table itself locks up and cannot be swiped. I can unfreeze it pretty simply by tapping one of my column header buttons.

enter image description here

I'm just completely at a loss for what might be causing the freeze. I am using an NSFetchedResultsController and here is my delegate code for that. It's pretty boiler plate (Update: not as boiler plate now. Using a batching approach):

// MARK: NSFetchedResultsController delegate methods

lazy var deletedSectionIndexes : NSMutableIndexSet = {
    return NSMutableIndexSet()
}()

lazy var insertedSectionIndexes : NSMutableIndexSet = {
    return NSMutableIndexSet()
}()

lazy var deletedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()

lazy var insertedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()

lazy var updatedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()


func controllerWillChangeContent(controller: NSFetchedResultsController) {

}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch(type) {

    case .Delete:
        if let indexPath = indexPath {
            self.deletedRowIndexPaths.appendDistinct(indexPath)
        }
    case .Update:
        if let indexPath = indexPath {
            self.updatedRowIndexPaths.appendDistinct(indexPath)
        }
    case .Insert:
        if let newIndexPath = newIndexPath {
            self.insertedRowIndexPaths.appendDistinct(newIndexPath)
        }
    case .Move:
        if let indexPath = indexPath, newIndexPath = newIndexPath {
            self.insertedRowIndexPaths.appendDistinct(newIndexPath)
            self.deletedRowIndexPaths.appendDistinct(indexPath)
        }
    }
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    switch(type) {

    case .Delete:
        self.deletedSectionIndexes.addIndex(sectionIndex)
    case .Insert:
        self.insertedSectionIndexes.addIndex(sectionIndex)
    default:
        break
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    self.tableView.beginUpdates()
    self.tableView.insertSections(self.insertedSectionIndexes, withRowAnimation: .None)
    self.tableView.deleteSections(self.deletedSectionIndexes, withRowAnimation: .None)

    self.tableView.insertRowsAtIndexPaths(self.insertedRowIndexPaths, withRowAnimation: .None)
    self.tableView.deleteRowsAtIndexPaths(self.deletedRowIndexPaths, withRowAnimation: .None)
    self.tableView.reloadRowsAtIndexPaths(self.updatedRowIndexPaths, withRowAnimation: .None)
    self.tableView.endUpdates()

    self.insertedSectionIndexes.removeAllIndexes()
    self.deletedSectionIndexes.removeAllIndexes()
    self.deletedRowIndexPaths.removeAll()
    self.insertedRowIndexPaths.removeAll()
    self.updatedRowIndexPaths.removeAll()        
}

The delete gets called in the didChangeObject delegate method, however, technically it's not a real delete. I am simply setting a property to -1 and then saving that element through the NSMangagedObjectContext--at which point the NSFRC seems to do the right thing which is remove it from the list of fetched objects which were fetched using this predicate:

NSPredicate(format: "account = %@ and quantity != -1", account)

where account is a valid account managed object. The row disappears without an issue 90% or more of the time. It's just on occasion that after the animation completes the table freezes in the manor I've described. It never freezes with the delete button still showing, so I know it's after commitEditingStyle gets called. The delete button does not have a custom implementation. It is the default UITableView implementation of swipe to delete. Here is my commitEditingStyle method:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        if let frameboardItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as? IRMFrameBoardItemMO {
            if frameboardItem.isNew {
                // If it's never been pushed to the server, just delete locally. This will trigger a table reload
                // via NSFetchedResultsController
                DataManager.mainContext.deleteObject(frameboardItem)
            } else {
                // Otherwise mark it with a negative quantity which tells the server to delete it and tells the
                // app to hide it.
                frameboardItem.quantity = -1
            }

            do {
                try DataManager.mainContext.save()
            } catch let error as NSError {
                dLog("Something went wrong: \(error.localizedDescription)")
            }

        }

    }

}

You can see a video here of what I'm talking about. It's over two minutes so you may not want to watch the whole thing, but I'll put it here for reference.

https://vimeo.com/153406113

Would love to hear any suggestions.

Update

I updated the NSFRC delegate methods to use a batching approach to ensure the updates get applied all at once. This has not fixed the issue. The table still freezes periodically.

like image 232
Matt Long Avatar asked Jan 28 '16 21:01

Matt Long


1 Answers

I also have guess about this issue. My idea is that controllerDidChangeContent can be called twice or more times and faster than table refreshes and this cause of multiple calls of tableView.beginUpdates() that can hangup table.

So to fix this I suggest wrap update in dispatch_async block, or just simple boolean flag

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
         self.tableView.beginUpdates()
         // ..... rest of update code 
         self.updatedRowIndexPaths.removeAll()
    })
}
like image 145
sage444 Avatar answered Oct 18 '22 13:10

sage444