NSFetchedResultsController inserts the same cell into two sections only when controller is about to insert another section

点点圈 提交于 2020-01-10 03:16:08

问题


This is my Message.swift file:

@objc(Message)
class Message: NSManagedObject {

    @NSManaged var content: String
    @NSManaged var createdAt: NSDate
    @NSManaged var identifier: Int64

    @NSManaged var conversation: Conversation

    @NSManaged var sender: Contributor

    var normalizedCreatedAt: NSDate {
        return createdAt.dateWithDayMonthAndYearComponents()!
    }
}

This is how I setup my FRC:

private func setupFetchedResultsController() {

    let context = NSManagedObjectContext.MR_defaultContext()
    let fetchRequest = NSFetchRequest(entityName: "Message")
    let createdAtDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)

    fetchRequest.predicate = NSPredicate(format: "conversation.identifier = %lld", conversation.identifier)
    fetchRequest.sortDescriptors = [createdAtDescriptor]

    fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: "normalizedCreatedAt", cacheName: nil)
    fetchedResultsController.delegate = self

    try! fetchedResultsController.performFetch()
    tableView.reloadData()
}

with its standard delegate.

On viewDidLoad my controller has 1 section with 1 row. I print it on console using the following function:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    print("--->>>")
    print(section)
    print(fetchedResultsController.sections![section].objects!.count)
    return fetchedResultsController.sections![section].objects!.count
}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return fetchedResultsController?.sections?.count ?? 0
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let message = fetchedResultsController?.objectAtIndexPath(indexPath) as! Message
    let cellIdentifier = message.sender.identifier == Settings.currentUser?.profile?.identifier ? SentTableViewCellIdentifier : ReceivedTableViewCellIdentifier
    let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! TableViewCell

    cell.cellTitleLabel?.text = message.content

    return cell
}

and the output is following:

--->>>
0
1

Once I try to add just ONE another message with different section, the following I get:

--->>>
0
2
--->>>
1
1

and then the error:

CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)

Why it happens like that?

NSFetchedResultsController for some reason loads the same cell into two sections: first and second. Why?

NOTE:

  • The problem arise ONLY when FRC insert new section. If it needs to insert row into existing section, there is no problem. Issue is strong related to sections.
  • The problem is ONLY when FRC try to insert a SECOND section. When it is about third or fourth section, there is no problem at all.

回答1:


I tried your code, made only one change, this:

var normalizedCreatedAt: String {
    return getTimeStrWithDayPrecision(createdAt!)
}

func getTimeStrWithDayPrecision(date: NSDate) -> String {
    let formatter = NSDateFormatter()
    formatter.timeStyle = .NoStyle
    formatter.dateStyle = .ShortStyle
    formatter.doesRelativeDateFormatting = true
    return formatter.stringFromDate(date)
}

and it works fine, even for 2nd section also!

For demo purpose, I added ADD button, by pressing it, code will add new message with current date string as content to DB.

Here is my complete implementation: View controller-

class ChatTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {


private var fetchedResultsController: NSFetchedResultsController?
private var _mainThreadMOC: NSManagedObjectContext?

override func viewDidLoad() {
    super.viewDidLoad()

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem()

    setupFetchedResultsController()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

private func getMainMOC() -> NSManagedObjectContext {
    if _mainThreadMOC == nil {
        let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
        _mainThreadMOC = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        _mainThreadMOC!.persistentStoreCoordinator = appDel.persistentStoreCoordinator
        _mainThreadMOC!.undoManager = nil
    }
    return _mainThreadMOC!
}

private func setupFetchedResultsController() {

    let fetchRequest = NSFetchRequest(entityName: "Message")
    let createdAtDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)
    fetchRequest.sortDescriptors = [createdAtDescriptor]

    fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: getMainMOC(), sectionNameKeyPath: "normalizedCreatedAt", cacheName: nil)
    fetchedResultsController!.delegate = self

    try! fetchedResultsController!.performFetch()
    tableView.reloadData()
}

@IBAction func addMessage(sender: AnyObject) {
    print("addMessage")

    let MOC = getMainMOC()
    let date = NSDate()
    let _ = Message(text: "\(date)", moc: MOC)
    do {
        try MOC.save()
    }catch {
        print("Error saving main MOC: \(error)")
    }

}

// MARK: - Table view data source

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return fetchedResultsController?.sections?.count ?? 0
}

override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let sectionInfo = fetchedResultsController!.sections! as [NSFetchedResultsSectionInfo]
    let title = sectionInfo[section].name

    let headerHeight:CGFloat = tableView.sectionHeaderHeight
    let headerLbl = UILabel(frame: CGRectMake(0, 0, tableView.frame.width, headerHeight))
    headerLbl.backgroundColor = UIColor.lightGrayColor()
    headerLbl.textAlignment = .Center
    headerLbl.text = title
    return headerLbl
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    print("--->>>")
    print(section)
    print(fetchedResultsController?.sections![section].objects!.count)
    return (fetchedResultsController?.sections![section].objects!.count)!
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let message = fetchedResultsController?.objectAtIndexPath(indexPath) as! Message
    let cell = tableView.dequeueReusableCellWithIdentifier("MessageCellId", forIndexPath: indexPath)

    cell.textLabel?.text = message.content!

    return cell
}
//MARK: - NSFetchedResultsControllerDelegate

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
}

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

    let indexSet = NSIndexSet(index: sectionIndex)

    switch type {
    case .Insert:

        tableView.insertSections(indexSet, withRowAnimation: .Fade)

    case .Delete:

        tableView.deleteSections(indexSet, withRowAnimation: .Fade)

    case .Update:

        fallthrough

    case .Move:

        tableView.reloadSections(indexSet, withRowAnimation: .Fade)
    }
}

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

    switch type {
    case .Insert:

        if let newIndexPath = newIndexPath {
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }

    case .Delete:

        if let indexPath = indexPath {
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }

    case .Update:

        if let indexPath = indexPath {
            tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }

    case .Move:

        if let indexPath = indexPath, let newIndexPath = newIndexPath {

            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
}
}

Message-

func getTimeStrWithDayPrecision(date: NSDate) -> String {
let formatter = NSDateFormatter()
formatter.timeStyle = .NoStyle
formatter.dateStyle = .ShortStyle
formatter.doesRelativeDateFormatting = true
return formatter.stringFromDate(date)
}

extension Message {

@NSManaged var content: String?
@NSManaged var createdAt: NSDate?

var normalizedCreatedAt: String {
    return getTimeStrWithDayPrecision(createdAt!)
}    
}

class Message: NSManagedObject {

// Insert code here to add functionality to your managed object subclass

override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
    super.init(entity: entity, insertIntoManagedObjectContext: context)
}

init(text: String, moc:NSManagedObjectContext) {
    let entity = NSEntityDescription.entityForName("Message", inManagedObjectContext: moc)
    super.init(entity: entity!, insertIntoManagedObjectContext: moc)
    content = text
    createdAt = NSDate()
}
}

Here is the iPad screenshot:

For testing multiple sections, I changed date & time setting os iPad.




回答2:


I do not know why, but to make it working you need to replace:

fetchedResultsController.sections![section].objects!.count

with

fetchedResultsController.sections![section].numberOfObjects

For some reason objects!.count returns incorrect number of objects opposite to numberOfObjects property.



来源:https://stackoverflow.com/questions/39201608/nsfetchedresultscontroller-inserts-the-same-cell-into-two-sections-only-when-con

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!