I have a strange problem where my cells in the tableview are not consistent. Sometimes they will show as a blank cell and other times they will load with the correct data. See the GIF below.
Notice the blank cell in section 1 changes each time.
I also have this problem when adding new cells to the tableview, but closing and reopening the app always fixes it. It just doesn't load correctly when getting added... but sometimes it does load correctly. See GIF Below.
I've been recommended to use the reloadData(), but that doesn't seem to help anything at all. I'm hoping someone will see this that will know what to do.
See Code Below
Table View Controller: (Swift)
import UIKit
import CoreData
class ListItemsTVC: UITableViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()
//var sequeItem: createItem?
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
self.tableView.rowHeight = 62
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = frc.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 28
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
let item = frc.objectAtIndexPath(indexPath) as! Items
cell.separatorInset = UIEdgeInsets(top: 0, left: 78, bottom: 0, right: 0)
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.price!))
//cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
return cell
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .Destructive, title: "Delete") { (action, indexPath) in
let item = self.frc.objectAtIndexPath(indexPath) as! Items
let id = item.id!
let request = self.fetchRequest()
let pred = NSPredicate(format: "%K == %#", "id",id)
request.predicate = pred
var fetchResults = [AnyObject]()
do {
fetchResults = try self.moc.executeFetchRequest(request)
} catch {
fatalError("Fetching Data to Delete Failed")
}
self.moc.deleteObject(fetchResults[0] as! NSManagedObject)
fetchResults.removeAtIndex(0)
do {
try self.moc.save()
} catch {
fatalError("Failed to Save after Delete")
}
}
return [delete]
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break
/*case NSFetchedResultsChangeType.Update:
tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break*/
default:
print("Default in didChangeSection was called")
break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
break
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
break
default:
print("Default in didChangeObject was called")
break
}
}
// MARK: - Custom Functions
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "section" , cacheName: nil)
return frc
}
func floatToCurrency(flt: Float) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
return String(formatter.stringFromNumber(flt)!)
}
}
Add Button View Controller: (Swift)
import UIKit
import CoreData
class AddItemListVC: UIViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var sendItem: Items?
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets and Actions
#IBAction func addItem(sender: AnyObject) {
let entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
if (NSUserDefaults.standardUserDefaults().objectForKey("nextItemID") == nil) {
NSUserDefaults.standardUserDefaults().setObject(1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
}
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
item.id = id
switch id {
case 1..<10:
item.name = "Item ID 00\(id)"
case 10..<100:
item.name = "Item ID 0\(id)"
default:
item.name = "Item ID \(id)"
}
item.brand = "Brand \(id)"
item.qty = 1
item.price = 0
item.size = "Size \(id)"
let sec: Int = Int(arc4random_uniform(UInt32(4 - 1))) + 1
item.section = "Section \(sec)"
item.isChecked = false
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
navigationController!.popViewControllerAnimated(true)
}
}
#Jason Brady, I have just downloaded your code.
There is no problem with you core data, array or table view.
When i run an app in iPhone 5 / iPhone 6 / iPhone 6 Plus with 8.1 it is working fine, none of cell or add button is getting hidden.
But with same devices with 9.2 there is a problem.
Solutions
(1) Custom cell with dequeueReusableCellWithIdentifier
let cell : ListItemsTVCell! = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
(2) DidselectedRowAtIndex - Here you will get information at cell selection, So data is going perfectly.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Select")
let myCell = tableView.cellForRowAtIndexPath(indexPath) as! ListItemsTVCell
print(myCell.itemName.text)
}
(3) Problem is with AutoLayout, when i disabled it, all label went to -X positions, when i have aligned it properly using auto resizing, it is now working fine. ( See attached screen shot )
So you need to check with AutoLayout, why it is giving problem in iOS 9 and newer.
Refer link
I hope you can figure out and resolve further.
All the best.
Download
Instead of using
tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath)
try using
tableView.dequeueReusableCellWithIdentifier("listContentCell")
Related
I'm trying to get sections to work in my NSFetchResultController (Using Swift 3). The sections are of a calculated property of a NSManagedObject. The sections are displayed just fine. But every time I try to add a new Object which would generate a new section the app crashes with the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 4. The number of rows contained in an existing section after the update (1) 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 (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
The FetchResultController part of my TableViewController looks as follows:
lazy var fetchedResultsController: NSFetchedResultsController<Package> = {
let fetchRequest = NSFetchRequest<Package>(entityName:"Package")
let sectionDescriptor = NSSortDescriptor(key: "sectionTitle", ascending: true)
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)
fetchRequest.sortDescriptors = [sectionDescriptor , sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: "sectionTitle", cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
managedObjectContext = appDelegate.persistentContainer.viewContext
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Package")
do {
try managedObjectContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
The TableView Data Source part of my TableViewController looks as follows:
override func numberOfSections(in tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
return 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier: String = "PackageTableCell"
let cell: PackageTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! PackageTableViewCell
let package = fetchedResultsController.object(at: indexPath)
cell.labelId.text = String(describing: package.packageId)
cell.labelTime.text = DateFormatter.localizedString(from: package.deliveryTime as! Date, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)
cell.labelWeight.text = String(describing: package.weight)
cell.labelBatch.text = String(describing: package.batch!.batchId)
cell.labelRecepients.text = String(describing: package.recepients)
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRow(at: indexPath as IndexPath, animated: true)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let package: Package = fetchedResultsController.object(at: indexPath)
managedObjectContext.delete(package)
}
}
The FetchResultController Delegate part of my TableViewController looks as follows:
func controllerWillChangeContent() {
tableView.beginUpdates()
}
func controllerDidChangeContent() {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
case .delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
case .move:
break
case .update:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! PackageTableViewCell
configureCell(cell: cell, atIndexPath: indexPath! as NSIndexPath)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
Then In my AddViewController I save the object as shown below:
#IBAction func savePressed(_ sender: UIBarButtonItem) {
if mode == .add {
let entity = NSEntityDescription.entity(forEntityName: "Package", in: self.managedObjectContext)
package = Package(entity: entity!, insertInto: self.managedObjectContext)
let packageId: Int = UserDefaults.standard.integer(forKey: "packageId")
package?.packageId = Int32(packageId)
UserDefaults.standard.set((packageId +1), forKey: "packageId")
package?.createdAt = NSDate()
package?.location = mapView.userLocation.location! as CLLocation
}
let calendar = NSCalendar.current
let selectedDate = pickerDate.date
let selectedTime = pickerTime.date
var deliveryTimeComponents = DateComponents()
deliveryTimeComponents.year = calendar.component(.year, from: selectedDate)
deliveryTimeComponents.month = calendar.component(.month, from: selectedDate)
deliveryTimeComponents.day = calendar.component(.day, from: selectedDate)
deliveryTimeComponents.hour = calendar.component(.hour, from: selectedTime)
deliveryTimeComponents.minute = calendar.component(.minute, from: selectedTime)
let deliveryTime = calendar.date(from: deliveryTimeComponents)
package?. deliveryTime = deliveryTime as NSDate?
package?.batch = batchPicker?.selectedBatch
package?.weight = Float(inputWeight.text!)!
package?.recepients = Int16(stepperRecepients.value)
package?.notes = textNotes.text!
do {
try package?.managedObjectContext?.save()
if mode == .edit {
packageTransferDelegate?. packageTransfer(transferPackage: package!)
}
dismiss(animated: true, completion: nil)
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
And finally my Package Class (Package+CoreDataClass)
import Foundation
import CoreData
public class Package: NSManagedObject {
var sectionTitle: String {
return DateFormatter.localizedString(from: self.deliveryTime! as Date, dateStyle: DateFormatter.Style.long, timeStyle: DateFormatter.Style.none)
}
}
Any tips on how I could resolve this problem would be very welcome. Also I'm fairly new to iOS development, so if you see something in my code which you don't like, feel free to write me about it 😉
I found the answer to my own question. I forgot to pass the NSFetchResultContoller into the controllerWillChangeContent and controllerDidChangeContent functions. Which lead to the crash.
I corrected the code as follows:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
Now everything seems to work just fine.
I've been working on an app, and ran into some problems, so I rebuilt it as simple as possible to find where the error happens at. My code uses CoreData, and my first example has the code for adding new items inside the plus button in the navigation bar. In other words, there is no change of view or segue. This version works as I would expect it to. See the following GIF.
But when I make a new view controller that adds the new item, I end up getting a blank cell instead. I have the exact same code behind the add button that I had in the plus button. And I just use a show segue to get from the tableview to a normal view controller. But if I quit the app and start it back up, it all shows correctly. See below.
If you are curious, I have the section it is adding to being a random number between 1 and 3. My guess is there is something I need to do with that segue but I don't have clue what it is. I'm very new at Swift so I have lots to learn.
Thank you in advance for any help you can give me. I think if I can solve this problem, I can finally move forward with developing my app. Thank you again. Below is my code for the case where it has a segue.
Table View Controller Code (Swift)
import UIKit
import CoreData
class ListItemsTVC: UITableViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
self.tableView.rowHeight = 62
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets and Actions
/*#IBAction func addItem(sender: AnyObject) {
let entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
if (NSUserDefaults.standardUserDefaults().objectForKey("nextItemID") == nil) {
NSUserDefaults.standardUserDefaults().setObject(1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
}
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
item.id = id
switch id {
case 1...9:
item.name = "Item ID: 0\(id)"
default:
item.name = "Item ID: \(id)"
}
item.brand = "Brand \(id)"
item.qty = 1
item.price = 0
item.size = "Size \(id)"
let sec: Int = Int(arc4random_uniform(UInt32(4 - 1))) + 1
item.section = "Section \(sec)"
item.isChecked = false
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
}*/
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = frc.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 28
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
let item = frc.objectAtIndexPath(indexPath) as! Items
cell.separatorInset = UIEdgeInsets(top: 0, left: 78, bottom: 0, right: 0)
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.price!))
//cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
return cell
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .Destructive, title: "Delete") { (action, indexPath) in
let item = self.frc.objectAtIndexPath(indexPath) as! Items
let id = item.id!
let request = self.fetchRequest()
let pred = NSPredicate(format: "%K == %#", "id",id)
request.predicate = pred
var fetchResults = [AnyObject]()
do {
fetchResults = try self.moc.executeFetchRequest(request)
} catch {
fatalError("Fetching Data to Delete Failed")
}
self.moc.deleteObject(fetchResults[0] as! NSManagedObject)
fetchResults.removeAtIndex(0)
do {
try self.moc.save()
} catch {
fatalError("Failed to Save after Delete")
}
}
return [delete]
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
/*case NSFetchedResultsChangeType.Update:
tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)*/
default:
print("Default in didChangeSection was called")
break
}
}
internal func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
default:
print("Default in didChangeObject was called")
break
}
}
/*
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: - Custom Functions
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "section" , cacheName: nil)
return frc
}
func floatToCurrency(flt: Float) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
return String(formatter.stringFromNumber(flt)!)
}
}
View Controller (Swift) [The segue view]
import UIKit
import CoreData
class AddItemListVC: UIViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//var frc: NSFetchedResultsController = NSFetchedResultsController()
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets and Actions
#IBAction func addItem(sender: AnyObject) {
let entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
if (NSUserDefaults.standardUserDefaults().objectForKey("nextItemID") == nil) {
NSUserDefaults.standardUserDefaults().setObject(1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
}
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
item.id = id
switch id {
case 1...9:
item.name = "Item ID: 0\(id)"
default:
item.name = "Item ID: \(id)"
}
item.brand = "Brand \(id)"
item.qty = 1
item.price = 0
item.size = "Size \(id)"
let sec: Int = Int(arc4random_uniform(UInt32(4 - 1))) + 1
item.section = "Section \(sec)"
item.isChecked = false
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Reload TableView Data is viewWillAppear method as :
override func viewWillAppear() {
super.viewWillAppear()
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
}
As when you are navigating back from viewController you are not telling the TableViewController to update tableView so do it in viewWillAppear as it will fire when you are popping navController to come back.
Here is the code that fixed the problem for me. I only changed the Table View Controller Code:
import UIKit
import CoreData
class ListItemsTVC: UITableViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
self.tableView.rowHeight = 62
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets and Actions
/*#IBAction func addItem(sender: AnyObject) {
let entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
if (NSUserDefaults.standardUserDefaults().objectForKey("nextItemID") == nil) {
NSUserDefaults.standardUserDefaults().setObject(1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
}
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
item.id = id
switch id {
case 1...9:
item.name = "Item ID: 0\(id)"
default:
item.name = "Item ID: \(id)"
}
item.brand = "Brand \(id)"
item.qty = 1
item.price = 0
item.size = "Size \(id)"
let sec: Int = Int(arc4random_uniform(UInt32(4 - 1))) + 1
item.section = "Section \(sec)"
item.isChecked = false
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
}*/
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = frc.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 28
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
let item = frc.objectAtIndexPath(indexPath) as! Items
cell.separatorInset = UIEdgeInsets(top: 0, left: 78, bottom: 0, right: 0)
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.price!))
//cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
return cell
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .Destructive, title: "Delete") { (action, indexPath) in
let item = self.frc.objectAtIndexPath(indexPath) as! Items
let id = item.id!
let request = self.fetchRequest()
let pred = NSPredicate(format: "%K == %#", "id",id)
request.predicate = pred
var fetchResults = [AnyObject]()
do {
fetchResults = try self.moc.executeFetchRequest(request)
} catch {
fatalError("Fetching Data to Delete Failed")
}
self.moc.deleteObject(fetchResults[0] as! NSManagedObject)
fetchResults.removeAtIndex(0)
do {
try self.moc.save()
} catch {
fatalError("Failed to Save after Delete")
}
}
return [delete]
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
/*case NSFetchedResultsChangeType.Update:
tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)*/
default:
print("Default in didChangeSection was called")
break
}
}
internal func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
default:
print("Default in didChangeObject was called")
break
}
}
/*
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: - Custom Functions
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "section" , cacheName: nil)
return frc
}
func floatToCurrency(flt: Float) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
return String(formatter.stringFromNumber(flt)!)
}
}
Muhammad's answer will surely solve the problem as it will trigger the reload of the table view every time viewWillAppear will call. But the problem is that is not a very good way of doing this.
You see, viewWillAppear gets called a lot of times. It is a UIViewController life cycle method. If you are going to the second view controller and coming back without pressing the add button, it will still reload the UITableView. Now, the point is not whether it is an expensive operation or not, the point is that viewWillAppear is not the place to do that kind of a stuff. What if you had to do something else before reloading the cell, you may not want to put it all there and risk creating bugs.
A good way of achieving this is through delegates (call backs). Read up more on this and you will see that it gives you a much needed flexibility.
I'm having a hard time when I have an item in my UITableView and I update the attribute value that determines the Section.
I have figured out it has something to do with didChangeSection and the option NSFetchedResultsChangeType.Update. But that is as far as I'm able to get.
I'm not sure what code I need to put in there that would update the number of rows in a section.
This is the exact error code I'm getting:
2016-04-17 20:00:37.126 EZ List[13722:1469523] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.60.7/UITableView.m:1716
2016-04-17 20:00:37.126 EZ List[13722:1469523] 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 (3), 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)
And this is the code in my view controller:
import UIKit
import CoreData
class ListItemsTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()
var list: Lists?
var catalog: Catalog?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.reloadData()
self.title = list?.name
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
// Uncomment the following line to preserve selection between presentations
//self.clearsSelectionOnViewWillAppear = true
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func viewWillAppear(animated: Bool) {
let imageView = UIImageView(image: UIImage(named: "TableBackground"))
imageView.contentMode = .ScaleAspectFill
self.tableView.backgroundView = imageView
self.tableView.tableFooterView = UIView(frame: CGRectZero)
}
override func viewDidAppear(animated: Bool) {
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
fatalError("Failed to perform inital fetch")
}
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = frc.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
override func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView
header.contentView.backgroundColor = UIColor(red: 84/255, green: 200/255, blue: 214/255, alpha: 0.5)
header.textLabel!.textColor = UIColor.whiteColor()
//header.alpha = 0.5 //make the header transparent
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTableViewCell
let item = frc.objectAtIndexPath(indexPath) as! Items
cell.separatorInset = UIEdgeInsets(top: 0, left: 78, bottom: 0, right: 0)
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.cost!))
cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
return cell
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
cell.backgroundColor = UIColor.clearColor()
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .Destructive, title: "Delete") { (action, indexPath) in
let request = self.fetchRequest()
var fetchResults = [AnyObject]()
do {
fetchResults = try self.moc.executeFetchRequest(request)
} catch {
fatalError("Fetching Data to Delete Failed")
}
self.moc.deleteObject(fetchResults[indexPath.row] as! NSManagedObject)
fetchResults.removeAtIndex(indexPath.row)
do {
try self.moc.save()
} catch {
fatalError("Failed to Save after Delete")
}
}
let edit = UITableViewRowAction(style: .Normal, title: "Edit") { (action, indexPath) in
// Code to come
}
let qty = UITableViewRowAction(style: .Normal, title: "Qty") { (action, indexPath) in
// Code to come
}
edit.backgroundColor = UIColor.init(red: 84/255, green: 200/255, blue: 214/255, alpha: 1)
return [delete, edit, qty]
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Update:
let cell = self.tableView.cellForRowAtIndexPath(indexPath!) as! ListItemsTableViewCell
let item = self.frc.objectAtIndexPath(indexPath!) as! Items
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.cost!))
cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
default:
print("didChangeObject Default was accessed")
break
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Update:
self.tableView.reloadData()
default:
print("didChangeSection Default was accessed")
break
}
}
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "section" , cacheName: nil)
return frc
}
func getCatalog(id: NSNumber) -> Catalog {
var cat: Catalog?
let fetchReq = NSFetchRequest(entityName: "Catalog")
let pred = NSPredicate(format: "%K == %#", "id", id)
fetchReq.predicate = pred
do {
let check = try moc.executeFetchRequest(fetchReq)
cat = (check.first as! Catalog)
} catch {
fatalError("Failed fetching Catalog Entry matching Item")
}
return cat!
}
func floatToCurrency(flt: Float) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
return String(formatter.stringFromNumber(flt)!)
}
/*
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var id: NSNumber
if (segue.identifier == "listItemView") {
let cell = sender as! UITableViewCell
let indexPath = self.tableView.indexPathForCell(cell)
let itemCont: ViewItemViewController = segue.destinationViewController as! ViewItemViewController
let item: Items = self.frc.objectAtIndexPath(indexPath!) as! Items
itemCont.item = item
id = item.id!
itemCont.catalog = getCatalog(id)
} else if (segue.identifier == "listItemViewEdit") {
let cell = sender as! UITableViewCell
let indexPath = self.tableView.indexPathForCell(cell)
let itemCont: AddItemListViewController = segue.destinationViewController as! AddItemListViewController
let item: Items = self.frc.objectAtIndexPath(indexPath!) as! Items
itemCont.item = item
id = item.id!
itemCont.catalog = getCatalog(id)
itemCont.list = list
}
}
}
I feel like I'm really close to getting it right, but I just need that extra push.
Try reloading the rows in the update you are talking about and check this if it helps
https://forums.developer.apple.com/thread/12184
So it turns out it wasn't a problem with didChangeSection and NSFetchedResultsChangeType.Update. It was a problem with didChangeObject and NSFetchedResultsChangeType.Move.
The following code fixed my issue. Everything else is the same, so I'll just put those two functions.
Added NSFetchedResultsChangeType.Move and inside it I delete the old location and and the new location.
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
/*case NSFetchedResultsChangeType.Update:
tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)*/
default:
print("didChangeSection Default was accessed")
break
}
}
Removed NSFetchedResultsChangeType.Update since the move takes care of the Section updating.
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Update:
let cell = self.tableView.cellForRowAtIndexPath(indexPath!) as! ListItemsTableViewCell
let item = self.frc.objectAtIndexPath(indexPath!) as! Items
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.cost!))
cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
case NSFetchedResultsChangeType.Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
Hopes this helps someone else!
I've read a few other questions on this that the problem related to the size of the CollectionView. I've tried adjusting the size as recommended in those answers, but none of them were working. I'm using a NSFetchedResultsController which convolutes it for me as well (I'm wondering if it has something to do with it not firing).
Anyways, the real problem is I have no content appearing for my UICollectionView. There are no errors at runtime, simply a blank screen. I'm working in Swift (obviously).
Here is my code from the ViewController:
import UIKit
import CoreData
private let reuseIdentifier = "Family"
private var selectedFirstName:String = "Blank"
private var selectedLastName:String = "Blank"
private var selectedNumber:String = "Blank"
private var selectedEmail:String = "Blank"
class FamilyViewController: UIViewController, UICollectionViewDataSource {
var coreDataStack: CoreDataStack!
var fetchedResultsController: NSFetchedResultsController!
#IBOutlet var familyCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
//1
let fetchRequest = NSFetchRequest(entityName: "Family")
let firstNameSort =
NSSortDescriptor(key: "firstName", ascending: true)
fetchRequest.sortDescriptors = [firstNameSort]
//2
self.coreDataStack = CoreDataStack()
fetchedResultsController =
NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.context,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = CollectionViewFetchedResultsControllerDelegate(collectionView: familyCollectionView)
//3
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
func configureCell(cell: FamilyCCell, indexPath: NSIndexPath) { let family = fetchedResultsController.objectAtIndexPath(indexPath) as! Family
cell.firstNameLabel.text = family.firstName
print("configureCell ran")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
print("numberOfSectionsInCollectionView ran")
return fetchedResultsController.sections!.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
let sectionInfo = fetchedResultsController.sections![section]
print("numberOfItemsInSection ran")
return sectionInfo.numberOfObjects
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! FamilyCCell
print("cellForItemAtIndexPath ran")
configureCell(cell, indexPath: indexPath)
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "showDetail") {
let detailVC = segue.destinationViewController as! ContactViewController
print("prepareForSegue ran")
detailVC.detailFirstName = selectedFirstName
detailVC.detailLastName = selectedLastName
detailVC.detailNumber = selectedNumber
detailVC.detailEmail = selectedEmail
}
}
}
extension FamilyViewController: UICollectionViewDelegate {
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print("didSelectItemAtIndexPath ran")
collectionView.delegate = self
let family = fetchedResultsController.objectAtIndexPath(indexPath) as! Family
selectedFirstName = family.firstName!
selectedLastName = family.lastName!
selectedNumber = family.phone!
selectedEmail = family.email!
coreDataStack.saveContext()
}
}
class CollectionViewFetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: Properties
private let collectionView: UICollectionView
private var blockOperations: [NSBlockOperation] = []
// MARK: Init
init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
// MARK: Deinit
deinit {
blockOperations.forEach { $0.cancel() }
blockOperations.removeAll(keepCapacity: false)
}
// MARK: NSFetchedResultsControllerDelegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
blockOperations.removeAll(keepCapacity: false)
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.insertItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Update:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Move:
guard let indexPath = indexPath else { return }
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.moveItemAtIndexPath(indexPath, toIndexPath: newIndexPath) }
blockOperations.append(op)
case .Delete:
guard let indexPath = indexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteItemsAtIndexPaths([indexPath]) }
blockOperations.append(op)
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
let op = NSBlockOperation { [weak self] in self?.collectionView.insertSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Update:
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Delete:
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
default: break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView.performBatchUpdates({
self.blockOperations.forEach { $0.start() }
}, completion: { finished in
self.blockOperations.removeAll(keepCapacity: false)
})
}
}
I have print statements confirming that the cellForItemAtIndexPath is not running. Any ideas? I realize this is quite specific and I gave a crap ton of code, just not quite sure where the error could be coming from. Thanks for any help in advance.
Make sure...you Confirm UICollectionViewDelegate protocol methods.
set collectionview.delegate = self
collectionview.datasource = self
in the collection view datasource for the methods numberOfSectionsInCollectionView return 1 and in collectionViewNumberOfCellsForSection return the number of cells you want to display
to clarify on tableview or a collection view a "section" is a group of related things where as a cell (or row for tableview) are the actual things you want to display - when you do not override the amount of cells required it will return 0 for all sections
(ill check the exact method signatures in the morning and edit if they are slightly off)
Write it:
class FamilyViewController: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate{
and write these two lines in your viewDidLoad method:
familyCollectionView.delegate = self
familyCollectionView.datasource = self
I have a list of customers in a UITableView managed by an NSFetchedResultsController, class is called CustomersViewController. When I select a customer, a new view controller CustomerDetailViewController is loaded which displays their details and then a list of radiators related to them in another UITableView managed by an NSFetchedResultsController. The only editing I need on the tables is Deletion and this works fine in both tables managed by NSFetchedResultsController.
I want to be able to edit the customers details, so I have an edit button in the NavigationBar that segues to EditCustomerViewController from CustomerDetailViewController. As with previous segues the managedObjectContext and the managedObject (the selected customer) is passed through successfully and I can access all the objects values in the EditCustomerViewController, what I can't seem to do is edit them without getting these errors:
2016-02-18 12:30:08.349 Radiator Calculator[13825:2113477] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.29.5/UITableView.m:1720
2016-02-18 12:30:08.351 Radiator Calculator[13825:2113477] 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 (2), plus or minus the number of rows inserted or deleted from that section (1 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)
From this error I am guessing the issue lies with the NSFetchedResultsController not liking me changing the value in the EditCustomerViewController two viewcontrollers ahead of where it was instantiated. Given that there is no table in this view controller I haven't set it up.
The code for the three viewcontrollers in question are:
Code for CustomersViewController:
import UIKit
import CoreData
class CustomersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {
var context: NSManagedObjectContext!
#IBOutlet weak var customerList: UITableView!
var selectedCustomer: NSManagedObject!
// MARK: - viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
print("Customers VC")
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
context = appDel.managedObjectContext
do {
try fetchedResultsController.performFetch()
} catch {
print("Error occured with FRC")
}
}
override func viewWillAppear(animated: Bool) {
//reload todo list data array
customerList.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: - Table data functions
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Customer Cell")
let customer = fetchedResultsController.objectAtIndexPath(indexPath)
print(customer)
cell.textLabel?.text = customer.name
return cell
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
context.deleteObject(customer)
do {
try context.save()
} catch let error as NSError {
print("Error saving context after delete \(error.localizedDescription)")
}
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedCustomer = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
self.performSegueWithIdentifier("customerDetailSegue", sender: self)
}
// MARK: - Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
print("seg fired")
if segue.identifier == "addCustomerSegue" {
if let addCustomerViewController = segue.destinationViewController as? AddCustomerViewController {
addCustomerViewController.context = context
}
}
if segue.identifier == "customerDetailSegue"{
if let customerDetailViewController = segue.destinationViewController as? CustomerDetailViewController {
customerDetailViewController.context = context
customerDetailViewController.customer = selectedCustomer
}
}
}
// set up frc
lazy var fetchedResultsController: NSFetchedResultsController = {
let customerFetchRequest = NSFetchRequest(entityName: "Customers")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
customerFetchRequest.sortDescriptors = [sortDescriptor]
let frc = NSFetchedResultsController(fetchRequest: customerFetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
//MARK: NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.customerList.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type{
case NSFetchedResultsChangeType.Insert:
//note that for insert we insert a row at _newIndexPath_
if let insertIndexPath = newIndexPath {
self.customerList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
case NSFetchedResultsChangeType.Delete:
//note that for delete we delete the row at _indexPath_
if let deleteIndexPath = indexPath {
self.customerList.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
case NSFetchedResultsChangeType.Update:
//note that for update we update the row at _indexPath_
if let updateIndexPath = indexPath {
let cell = self.customerList.cellForRowAtIndexPath(updateIndexPath)
let customer = fetchedResultsController.objectAtIndexPath(updateIndexPath)
cell!.textLabel?.text = customer.name
}
case NSFetchedResultsChangeType.Move:
//note that for Move we delete the row at _indexPath_
if let deleteIndexPath = indexPath {
self.customerList.insertRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
//note that for move we insert a row at _newIndexPath_
if let insertIndexPath = newIndexPath {
self.customerList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
//note needed as only have one section
}
func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
return sectionName
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.customerList.endUpdates()
}
}
and CustomerDetailViewController
import UIKit
import CoreData
class CustomerDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {
var context: NSManagedObjectContext!
var customer: NSManagedObject!
#IBOutlet weak var customerName: UILabel!
#IBOutlet weak var street: UILabel!
#IBOutlet weak var town: UILabel!
#IBOutlet weak var postCode: UILabel!
#IBOutlet weak var radiatorList: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
radiatorList.allowsSelection = false
if let name = customer.valueForKey("name") as? String {
customerName.text = name
}
if let addressLine1 = customer.valueForKey("address_line_1") as? String {
street.text = addressLine1
}
if let townName = customer.valueForKey("town") as? String {
town.text = townName
}
if let postcode = customer.valueForKey("postcode") as? String {
postCode.text = postcode
}
// set up FRC
do {
try fetchedResultsController.performFetch()
} catch {
print("Error occured with FRC")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Radiator Cell", forIndexPath: indexPath) as! RadiatorCell
let radiator = fetchedResultsController.objectAtIndexPath(indexPath) as? NSManagedObject
if let radiatorName = radiator?.valueForKey("radiatorName") as? String {
cell.radNameLabel.text = String(radiatorName)
}
if let radiatorPowerWatts = radiator?.valueForKey("radiatorPowerWatts") as? Double{
print(radiatorPowerWatts)
cell.radPowerWattsLabel.text = "\(ceil(radiatorPowerWatts)) Watts"
cell.radPowerBtusLabel.text = "\(ceil(radiatorPowerWatts / 0.293)) BTUs"
}
return cell
}
/*
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
*/
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
let radiator = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
context.deleteObject(radiator)
do {
try context.save()
} catch let error as NSError {
print("Error saving context after delete \(error.localizedDescription)")
}
}
}
// set up frc
lazy var fetchedResultsController: NSFetchedResultsController = {
let radiatorFetchRequest = NSFetchRequest(entityName: "Radiators")
let sortDescriptor = NSSortDescriptor(key: "radiatorName", ascending: true)
radiatorFetchRequest.predicate = NSPredicate(format: "customer = %#", self.customer)
radiatorFetchRequest.sortDescriptors = [sortDescriptor]
let frc = NSFetchedResultsController(fetchRequest: radiatorFetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
//MARK: NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.radiatorList.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type{
case NSFetchedResultsChangeType.Insert:
//note that for insert we insert a row at _newIndexPath_
if let insertIndexPath = newIndexPath {
self.radiatorList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
case NSFetchedResultsChangeType.Delete:
//note that for delete we delete the row at _indexPath_
if let deleteIndexPath = indexPath {
self.radiatorList.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
case NSFetchedResultsChangeType.Update:
//note that for update we update the row at _indexPath_
if let updateIndexPath = indexPath {
let cell = self.radiatorList.cellForRowAtIndexPath(updateIndexPath)
let radiator = fetchedResultsController.objectAtIndexPath(updateIndexPath)
cell!.textLabel?.text = radiator.name
}
case NSFetchedResultsChangeType.Move:
//note that for Move we delete the row at _indexPath_
if let deleteIndexPath = indexPath {
self.radiatorList.insertRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
//note that for move we insert a row at _newIndexPath_
if let insertIndexPath = newIndexPath {
self.radiatorList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
//note needed as only have one section
}
func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
return sectionName
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.radiatorList.endUpdates()
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "mapSegue"{
if let mapVC = segue.destinationViewController as? CustomerMapViewController{
var addressString = String()
if let addressLine1 = customer.valueForKey("address_line_1") as? String {
let s = addressLine1
addressString += "\(s), "
}
if let townName = customer.valueForKey("town") as? String {
let t = townName
addressString += "\(t), "
}
if let postcode = customer.valueForKey("postcode") as? String {
let p = postcode
addressString += "\(p)"
}
mapVC.address = addressString
}
}
if segue.identifier == "editCustomerSegue" {
if let editVC = segue.destinationViewController as? EditCustomerViewController {
editVC.context = context
editVC.customer = customer
}
}
}
}
and
import UIKit
import CoreData
class EditCustomerViewController: UIViewController {
var context: NSManagedObjectContext!
var customer: NSManagedObject!
override func viewDidLoad() {
super.viewDidLoad()
print("Edit customer view controller")
if let name = customer.valueForKey("name") as? String {
//this works
print(name)
}
customer.setValue("Hardcoded name change", forKey: "name")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
The application keeps running until I navigate back to the CustomersViewController when it finally crashes and I can briefly see that the customer name has changed in the list - it hasn't changed in the CustomerDetailViewController however.
Any help would be great, apologies for any lack of "swifty-ness" (I read that's a thing) - this is my first larger app in swift and iOS so I'm still learning as I go.