问题
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