Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
496 views
in Technique[技术] by (71.8m points)

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

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.
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:
enter image description here

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...