NSFetchedResultsController 仅在控制器即将插入另一个部分时将相同的单元格插入两个部分

Posted

技术标签:

【中文标题】NSFetchedResultsController 仅在控制器即将插入另一个部分时将相同的单元格插入两个部分【英文标题】:NSFetchedResultsController inserts the same cell into two sections only when controller is about to insert another section 【发布时间】:2016-08-29 08:22:18 【问题描述】:

这是我的Message.swift 文件:

@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()!
    

这就是我设置 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()

与其标准委托。

viewDidLoad 上,我的控制器有 1 个部分和 1 行。我使用以下功能在控制台上打印它:

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

输出如下:

--->>>
0
1

一旦我尝试添加一条不同部分的另一条消息,我会得到以下信息:

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

然后是错误:

CoreData:错误:严重的应用程序错误。在调用 -controllerDidChangeContent: 期间,从 NSFetchedResultsController 的委托中捕获了异常。无效更新:第 0 节中的行数无效。更新后现有节中包含的行数 (2) 必须等于更新前该节中包含的行数 (1),加上或减去数字从该部分插入或删除的行数(0 插入,0 删除)加上或减去移入或移出该部分的行数(0 移入,0 移出)。与 userInfo (null)

为什么会这样?

NSFetchedResultsController 出于某种原因将同一个单元格加载到两个部分:firstsecond。为什么?

注意:

仅当 FRC 插入新部分时才会出现此问题。如果它需要将行插入现有部分,没有问题。问题与部分密切相关。 问题仅在 FRC 尝试插入 SECOND 部分时出现。到了第三、第四节的时候,完全没有问题。

【问题讨论】:

你能添加numberOfSectionsInTableViewcellForRowAtIndexPath的代码吗? 是的,我更新了问题。 代码看起来不错,没有模棱两可的地方。只需检查createdAt 属性是否正确,dateWithDayMonthAndYearComponents() 方法工作正常。 是的,它工作正常......他们两个...... :( 我也在使用完全相同的代码。还是,让我再检查一下…… 【参考方案1】:

我试过你的代码,只做了一个改动,这个:

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)

而且效果很好,即使是第二部分!

出于演示目的,我添加了ADD按钮,按下它,代码会将当前日期字符串为content的新消息添加到DB。

这是我的完整实现: 视图控制器-

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()


留言-

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()


这是 iPad 截图:

为了测试多个部分,我更改了 iPad 上的日期和时间设置。

【讨论】:

我认为这与String 无关。我将 normalizedCreatedAt 替换为 content 以创建部分,但问题仍然存在。 好的,我现在试试。 我用你的建议替换了normalizedCreatedAt,但还是一样...:(我应该做更多吗? 您是否使用单独的演示项目检查过这一点,或者您正在对现有项目进行更改?尝试从上面实现一切,包括创建和保存消息。 你保存的和我不一样,因为我用magicRecord,但是一切都是一样的......【参考方案2】:

我不知道为什么,但要使其正常工作,您需要更换:

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

fetchedResultsController.sections![section].numberOfObjects

由于某种原因,objects!.count 返回与numberOfObjects 属性相反的不正确对象数。

【讨论】:

以上是关于NSFetchedResultsController 仅在控制器即将插入另一个部分时将相同的单元格插入两个部分的主要内容,如果未能解决你的问题,请参考以下文章

在 Core Data 应用程序中调用 performFetch 后,是不是需要手动更新表视图?