Split core data table view into sections - ios

Currently my table view loads a long list of items NSMutableArray *certificates;
I want to group these into sections based on the certificateType. So far I have grouped the sections but need add the data to the sections based on the certificateType
The UITableView is populated from core data object Certificate
Can I sort these into sections using the name of cell.textLabel.text = [NSString stringWithFormat:#"%# (%#)", thisCert.certificateType.title, thisCert.title];
Or should I split up NSMutableArray *certificates; into 4 separate arrays
Any advice on best way to approach this would be appreciated
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
LogCmd();
return 4;
}
//Section Titles
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
//Section Text
[[UILabel appearanceWhenContainedIn:[UITableViewHeaderFooterView class], nil] setFont:[UIFont systemFontOfSize:18]];
//Section Titles
NSArray *sectionTitles = [NSArray arrayWithObjects:#"BlueCert", #"GreenCert", #"RedCert", #"OrangeCert", nil];
NSString *result = [sectionTitles objectAtIndex:section];
return result;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 60.0f;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return self.searchResults.count;
}
else {
return self.certificates.count;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
LogCmd();
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [UIFont boldSystemFontOfSize:14.0f];
cell.imageView.image = [UIImage imageNamed:#"IconFolderOpen"];
}
Certificate *thisCert;
if (tableView == self.searchDisplayController.searchResultsTableView) {
thisCert = (self.searchResults)[indexPath.row];
}
else {
thisCert = (self.certificates)[indexPath.row];
}
cell.textLabel.text = [NSString stringWithFormat:#"%# (%#)", thisCert.certificateType.title, thisCert.title];
cell.detailTextLabel.text = [NSString stringWithFormat:#"Reference: %#", thisCert.reference];
return cell;
}

If your data is dynamic, I suggest that you set up an NSFetchedResultsController.
That way your data is still together in the managed object context, but the data source for the table view can be broken up into sections based on title. Set the sectionNameKeyPath as certificate title for the NSFetchedResultsController. Set a sort descriptor based on certificate title for the NSFetchRequest used to get data into the NSFetchedResultsController.
Implement your view controller as the delegate for the NSFetchedResultsController.
Adding some high level code for elaboration. Most of this code is styled from the book Core Data Data Storage and Management for Ios, OS X, and Icloud by Marcus Zarra.
Assuming you have a managed object context and your are adding properties for NSFetchedResultsController and NSFetchRequest in your view controller
- (NSFetchedResultsController *) certificatesFetchedResultsController{
if(!_certificatesFetchedResultsController){
_certificatesFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:self.certificatesFetchRequest managedObjectContext:self.certitificatesMOC sectionNameKeyPath:#"title" cacheName:nil];
_certificatesFetchedResultsController.delegate = self;
}
return _certificatesFetchedResultsController;
}
- (NSFetchRequest *) certificatesFetchRequest{
if(!_certificatesFetchRequest){
_certificatesFetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Certificate"];
NSSortDescriptor *sortTitle = [[NSSortDescriptor alloc] initWithKey:#"title" ascending:NO];
[_certificatesFetchRequest setSortDescriptors:#[sortTitle]];
}
return _certificatesFetchRequest;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller{
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self transactionsTblView] insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self transactionsTblView] deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath{
NSArray *newArray = #[[NSIndexPath indexPathForItem:newIndexPath.row inSection:newIndexPath.section];
NSArray *oldArray = #[[NSIndexPath indexPathForItem:indexPath.row inSection:indexPath.section];
switch(type) {
case NSFetchedResultsChangeInsert:
[self.transactionsTblView insertRowsAtIndexPaths:newArray withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.transactionsTblView deleteRowsAtIndexPaths:oldArray withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
NSManagedObject *object = [self.certificatesFetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]];
// Create your section title using data in the managed object and return the UIView
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *object = [self.certificatesFetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]];
// Configure cell data as per data in managed object
}
Somewhere, initialze the fetched result controller. Maybe in viewDidLoad
[self.certificatesFetchedResultsController performFetch:nil];

Related

ios Core Data: make sure object is deleted from data source

UPDATE: In the comments someone pointed out that I was unnecessarily dispatching to the main thread. After removing the dispatches and unnecessary begin/end updates, now when I try to delete a cell, it calls didChangeObject with case NSFetchedResultsChangeUpdate (as opposed to NSFetchedResultsChangeDelete), which calls configureCell.
The line that crashes the program is CollectedLeaf* theCollectedLeaf = [collectionFetchedResultsController objectAtIndexPath:indexPath]; in the below method. The crash log is, no section at index 20 in sections list.
- (void)configureCell:(SpeciesCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
CollectedLeaf* theCollectedLeaf = [collectionFetchedResultsController objectAtIndexPath:indexPath];
[cell setCollectedLeaf:theCollectedLeaf];
}
I am getting an Invalid update: invalid number of rows in section error every time I swipe to delete a cell from my table. I get the breakpoint specifically at [_table endUpdates] in controllerDidChangeContent.
In many SO posts, the reason for this error is that the object had not been deleted from data source before deleting the row from the table. I delete the object from the data source in commitEditingStyle, which is called before deleteRowsAtIndexPaths in didChangeObject.
The fact that I'm still getting the Invalid update despite my order leads me to think that I am not properly/successfully deleting it from the data source. I am still new to iOS - how can I make sure an object is removed from my core data?
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[_table beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[_table beginUpdates];
[_table insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
[_table endUpdates];
break;
case NSFetchedResultsChangeDelete:
NSLog(#"delete called");
[_table deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
// [self configureCell:[_table cellForRowAtIndexPath:indexPath]
// atIndexPath:indexPath];
[_table reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
NSLog(#"fetched update");
break;
case NSFetchedResultsChangeMove:
[_table deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[_table insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[_table insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[_table deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[_table endUpdates];
}
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(_segmentedControl.selectedSegmentIndex == 1) {
NSLog(#"index path at editingStyleForRowAtIndexPath %#", indexPath);
return UITableViewCellEditingStyleDelete;
}
return NULL;
}
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
// [super tableView: tableView didEndEditingRowAtIndexPath:indexPath];
if(_segmentedControl.selectedSegmentIndex == 1) {
for (UITableViewCell *cell in [_table visibleCells]) {
for (UIView *subview in cell.contentView.subviews)
[subview.layer removeAllAnimations];
}
}
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
/*Only allow deletion for collection table */
if(_segmentedControl.selectedSegmentIndex == 1) {
if (editingStyle == UITableViewCellEditingStyleDelete)
{
NSLog(#"index path at commitediting style %#", indexPath);
CollectedLeaf* collectedLeaf = [collectionFetchedResultsController objectAtIndexPath:indexPath];
LeafletPhotoUploader * leafletPhotoUploader = [[LeafletPhotoUploader alloc] init];
leafletPhotoUploader.collectedLeaf = collectedLeaf;
if([LeafletUserRegistration isUserRegistered]) {
[leafletPhotoUploader deleteCollectedLeaf:collectedLeaf delegate:self];
}
// Delete the managed object for the given index path
NSManagedObjectContext *context = [collectionFetchedResultsController managedObjectContext];
[context deleteObject:[collectionFetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error;
if (![context save:&error])
{
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0)
{
for(NSError* detailedError in detailedErrors)
{
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else
{
NSLog(#" %#", [error userInfo]);
}
}
}
}
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"speciesCell";
SpeciesCell* speciesCell = (SpeciesCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self configureCell:speciesCell atIndexPath:indexPath];
speciesCell.labelCheckMark.backgroundColor = [UIColor blackColor];
return speciesCell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
id <NSFetchedResultsSectionInfo> sectionInfo = [[collectionFetchedResultsController sections] objectAtIndex:section];
if([sectionInfo numberOfObjects] == 0){
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, tableView.bounds.size.height)];
noDataLabel.text = #"Press the Snap It! tab to start collecting!";
//Start your collection by tapping on the Snap It! tab.";
noDataLabel.textColor = [UIColor whiteColor];
noDataLabel.textAlignment = NSTextAlignmentCenter;
tableView.backgroundView = noDataLabel;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return [sectionInfo numberOfObjects];
}
This post saved me
objectAtIndexPath is pretty buggy and can only be called within a bounds check, so I now do all of my work with the core data object within the nest if statement
CollectedLeaf* theCollectedLeaf = nil;
if ([[collectionFetchedResultsController sections] count] > [indexPath section]){
id <NSFetchedResultsSectionInfo> sectionInfo = [[collectionFetchedResultsController sections] objectAtIndex:[indexPath section]];
if ([sectionInfo numberOfObjects] > [indexPath row]){
theCollectedLeaf = [collectionFetchedResultsController objectAtIndexPath:indexPath];
//do whatever else I need, delete the object, etc
}
}
As the linked post describes,
"The reason you are getting an exception is that the state of the
NSFetchedResultsController is getting out of sync with the state of
the tableview. When your crash happens the tableView just asked your
delegate methods (numberOfSectionsInTableView and
tableView:numberOfRowsInSection: for the number of rows and sections.
When it asked that, the NSFetchedResultsController had data in it, and
gave positive values (1 section, 3 rows). But in-between that
happening and your crash, the entities represented by the
NSFetchedResultsController were deleted from the
NSManagedObjectContext. Now cellForRowAtIndexPath: is being called
with an outdated indexPath parameter."

Most efficient method for updating UIButton elements in a collection view for iOS Core data-driven app

I'm at a bit of a fork in the road and could use some advice. I'm building a sort-of punch card system and saving the state of the punch via core data (See screen shot below) :
Image Link : http://postimg.org/image/idrm8sfr3/
The app basically has a 'dates' collection view at the top. A table view of 'parent items' each with their own collection views of 'reps' or 'punches'. The collection view sync with one another when scrolling left or right in order to correlate to the corresponding date.
I've gotten it to a point where when the user taps a button/'punch' it creates a rep/punch managed object, attaches it to it's parent item (via core data relationships) sets the correlating date value, and turns it ON.
This is all well and good, however now I am trying to figure out the best way to update the UIButton representations of the Managed Reps/punches objects. My current theory is to perform a fetch request on load of the UIButton. This would cycle through the parent item's 'child' items (reps/punches) and search for a matching date. Once it finds an item with the matching date, it response accordingly to the object, in that whether it is on or off. In my mind, since the cells in the collection views are dequed, it would only perform a maximum of about 35 fetch requests at once, since this is all the cells that will fit on the screen. Then it will perform new requests once new cells are loaded. This would prevent having to load every rep/punch object for all of time once the view is loaded.
My other thoughts would be to have a correlating rep/punch managed object created for ever cell and just synchronize the cells based on an index path, however the app would end up having a lot of 'dead' objects that are just sitting there. This seems wasteful and inefficient.
Finally the third option I thought of would be to some how create a dictionary for each parent item's set of child managed objects and make the key be their date values. Then I could simply sync the dictionary keys to the corresponding cell's date values.
I sincerely hope this is not a bunch of gibberish! I'm new to iOS programming and trying my best to convey my challenge with my limited scope. Please let me know if you have any questions.
Relevant code so far :
#interface UpdateRepsViewController ()
#property (weak, nonatomic) IBOutlet UICollectionView *datesCV;
#property (weak, nonatomic) IBOutlet UITableView *habitsTableView;
#end
#implementation UpdateRepsViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// scroll to last item in collection view (ie: today)
NSInteger lastItem = [[Calculations updateRepsDateRangeDates] count]-1;
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:lastItem inSection:0];
[self.datesCV scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionRight animated:YES];
self.horScrollPos = self.datesCV.contentOffset.x;
NSLog(#"%f", self.horScrollPos);
// hides separator lines
self.habitsTableView.separatorColor = [UIColor clearColor];
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Error : %#", error);
abort();
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - managedObjectContext Reference
-(NSManagedObjectContext *)managedObjectContext {
return [(AppDelegate *) [[UIApplication sharedApplication] delegate] managedObjectContext];
}
#pragma mark - Scroll Sync & Control
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.datesCV) {
for (UpdateRepsHabitCell *x in [self.habitsTableView visibleCells]) {
x.repsCV.contentOffset = CGPointMake(scrollView.contentOffset.x, 0);
}
}
if (scrollView.tag == 5) {
for (UpdateRepsHabitCell *x in [self.habitsTableView visibleCells]) {
x.repsCV.contentOffset = CGPointMake(scrollView.contentOffset.x, 0);
}
self.datesCV.contentOffset = CGPointMake(scrollView.contentOffset.x, 0);
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.horScrollPos = scrollView.contentOffset.x;
NSLog(#"%f", self.horScrollPos);
}
#pragma mark - CollectionViews Data Source
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (collectionView == self.datesCV) {
return [[Calculations updateRepsDateRangeDates] count];
}
if (collectionView.tag == 5) {
return [[Calculations updateRepsDateRangeDates] count];
}
else return 3;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.datesCV) {
UpdateRepsDateCell *cell = (UpdateRepsDateCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"DateCell" forIndexPath:indexPath];
NSDateFormatter *df = [[NSDateFormatter alloc]init];
[df setDateFormat:#"EEEEE\nM/d"];
cell.date.text = [df stringFromDate:[Calculations updateRepsDateRangeDates][indexPath.row]];
return cell;
}
if (collectionView.tag == 5) {
RepButtonCell *cell = (RepButtonCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"RepCell" forIndexPath:indexPath];
// get cell date
NSDate *cellDate = [Calculations updateRepsDateRangeDates][indexPath.row];
// get parent habit
UpdateRepsRepCV * urcv = (UpdateRepsRepCV *)collectionView;
Habit *habit = [self.fetchedResultsController objectAtIndexPath:urcv.habitIndexPath];
// show relevant buttons
if ([cellDate compare:habit.startDate] >= 0) {
cell.repButton.hidden = NO;
}
else {
cell.repButton.hidden = YES;
}
cell.repButton.habitIndexPath = urcv.habitIndexPath;
cell.repButton.cellIndexPath = indexPath;
return cell;
}
else {
UICollectionViewCell *cell = [[UICollectionViewCell alloc]init];
return cell;
}
}
#pragma mark - Habits Table View Data Source
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> secInfo = [self.fetchedResultsController.sections objectAtIndex:section];
return [secInfo numberOfObjects];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UpdateRepsHabitCell *cell = [tableView dequeueReusableCellWithIdentifier:#"HabitCell" forIndexPath:indexPath];
Habit *habit = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.habitName.text = habit.name;
cell.parentTableView = self.habitsTableView;
cell.repsCV.habitIndexPath = indexPath;
// set scroll
cell.repsCV.contentOffset = CGPointMake(self.horScrollPos, 0);
return cell;
}
#pragma mark - NSFetchedResultsController
-(NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Habit" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]initWithKey:#"sortOrder" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
#pragma mark - NSFetchedResultsController Delegate Methods
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.habitsTableView beginUpdates];
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.habitsTableView endUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.habitsTableView;
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
Habit *changedHabit = [self.fetchedResultsController objectAtIndexPath:indexPath];
UpdateRepsHabitCell *cell = (UpdateRepsHabitCell *)[tableView cellForRowAtIndexPath:indexPath];
cell.habitName.text = changedHabit.name;
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
-(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch (type) {
case NSFetchedResultsChangeInsert:
[self.habitsTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.habitsTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
#pragma mark - IBActions
- (IBAction)repPressed:(id)sender {
// get parent habit
RepButton *repButton = (RepButton *)sender;
Habit *parentHabit = [self.fetchedResultsController objectAtIndexPath:repButton.habitIndexPath];
// get date
NSDate *repDate = [Calculations updateRepsDateRangeDates][repButton.cellIndexPath.row];
Rep *rep = (Rep*)[NSEntityDescription insertNewObjectForEntityForName:#"Rep" inManagedObjectContext:self.managedObjectContext];
rep.habit = parentHabit;
rep.date = repDate;
rep.completed = [NSNumber numberWithBool:YES];
NSError *error;
if ( ! [[self managedObjectContext]save:&error]) {
NSLog(#"An error! %#", error);
}
}

Putting number of rows in section title

I would need to include the number of rows in the section title, like TODAY (6), that means that on section titled TODAY there are 6 rows (core data objects in my case). I know that it is to find here:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections]objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
But I don't know how to include this on each section to be shown as a label text.
Any advice is welcome....
#import "ToDoItemsTableViewController.h"
#import "AppDelegate.h"
#import "AddToDoItemViewController.h"
#import "ToDoSubItemsTableViewController.h"
#interface ToDoItemsTableViewController ()
#property (nonatomic, strong)NSManagedObjectContext *managedObjectContext;
#property (nonatomic, strong)NSFetchedResultsController *fetchedResultsController;
#end
#implementation ToDoItemsTableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
-(NSManagedObjectContext *)managedObjectContext{
return [(AppDelegate*)[[UIApplication sharedApplication]delegate]managedObjectContext];
}
- (void)viewDidLoad
{
[super viewDidLoad];
NSError *error = nil;
if (![[self fetchedResultsController]performFetch:&error]){
NSLog(#"Error %#",error);
abort();
}
}
-(void) viewWillAppear:(BOOL)animated{
[self.tableView reloadData];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([[segue identifier]isEqualToString:#"addToDoItem"]){
UINavigationController *navigationController = segue.destinationViewController;
AddToDoItemViewController *addToDoItemViewController = (AddToDoItemViewController*)navigationController.topViewController;
ToDoItem *addToDoItem = [NSEntityDescription insertNewObjectForEntityForName:#"ToDoItem" inManagedObjectContext:self.managedObjectContext];
addToDoItemViewController.addToDoItem = addToDoItem;
}
if ([[segue identifier] isEqualToString:#"toToDoSubItems"]){
ToDoSubItemsTableViewController *todoSubItemsTableViewController = [segue destinationViewController];
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
ToDoItem *selectedToDoItem = (ToDoItem*)[self.fetchedResultsController objectAtIndexPath:indexPath];
todoSubItemsTableViewController.selectedToDoItem = selectedToDoItem;
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections]count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections]objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
ToDoItem *todoItem = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = todoItem.todoName;
NSDate *fechaToDO = todoItem.todoDueDate;
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:#"EEEE, dd MMMM YYYY"];
NSString *fechaToDo = [dateFormatter stringFromDate:fechaToDO];
cell.detailTextLabel.text = fechaToDo;
return cell;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
static NSString *header = #"customHeader";
UITableViewHeaderFooterView *vHeader;
vHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:header];
if (!vHeader) {
vHeader = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:header];
vHeader.textLabel.backgroundColor = [UIColor redColor];
vHeader.textLabel.textColor = [UIColor whiteColor];
vHeader.contentView.backgroundColor = [UIColor redColor];
}
if (section == 1) {
vHeader.textLabel.backgroundColor = [UIColor blueColor];
vHeader.textLabel.textColor = [UIColor whiteColor];
vHeader.contentView.backgroundColor = [UIColor blueColor];
} else {
[vHeader setBackgroundColor:[UIColor redColor]];
}
vHeader.textLabel.text = [self tableView:tableView titleForHeaderInSection:section];
return vHeader;
}
-(NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
id <NSFetchedResultsSectionInfo> theSection = [[self.fetchedResultsController sections]objectAtIndex:section];
NSString *sectionname = [theSection name];
if ([sectionname isEqualToString:#"0"]){
return #"Overdue";
}
else if ([sectionname isEqualToString:#"1"]){
return #"Today";
}
else if ([sectionname isEqualToString:#"2"]){
return #"Upcoming";
}
if ([[self.fetchedResultsController sections]count]>0){
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections]objectAtIndex:section];
return [sectionInfo name];
}
else{
return nil;
}
}
#pragma mark - Fetched Results Controller Section
-(NSFetchedResultsController*)fetchedResultsController{
if (_fetchedResultsController != nil){
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
NSManagedObjectContext *context = self.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"ToDoItem" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]initWithKey:#"todoDueDate" ascending:YES];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]initWithKey:#"todoName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc]initWithObjects:sortDescriptor,sortDescriptor1, nil];
fetchRequest.sortDescriptors = sortDescriptors;
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"sectionIdentifier" cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
#pragma mark - Fetched Results Controller Delegates
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
-(void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath{
UITableView *tableView = self.tableView;
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:{
ToDoItem *changeToDoItem = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.textLabel.text = changeToDoItem.todoName;
cell.detailTextLabel.text = changeToDoItem.todoDescription;
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
-(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
/*
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
*/
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
NSManagedObjectContext *context = [self managedObjectContext];
ToDoItem *ToDoItemToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
[context deleteObject:ToDoItemToDelete];
NSError *error = nil;
if (![context save:&error]){
NSLog(#"Error: %#",error);
}
}
}
/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
}
*/
/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the item to be re-orderable.
return YES;
}
*/
/*
#pragma mark - Navigation
// In a story board-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#end
[NSString stringWithFormat:#"Today(%d)", [tableView numberOfRowsInSection:
section]];

After deleting last row of a section, the app crashes

In my iOS app ,I have a table view with sections, all populated from core data.
If a delete the last row of a section, the app crashes.
I am not able to find out where is the issue in my code, you are kindly requested to help me. Thank you in advance. Here is my code so far:
#import "PersonsTVC.h"
#import "Person.h"
#implementation PersonsTVC
#synthesize fetchedResultsController = __fetchedResultsController;
#synthesize managedObjectContext = __managedObjectContext;
#synthesize selectedPerson;
#synthesize searchResults,titulosseccion;
- (void)setupFetchedResultsController
{
// 1 - Decide what Entity you want
NSString *entityName = #"Person"; // Put your entity name here
NSLog(#"Setting up a Fetched Results Controller for the Entity named %#", entityName);
// 2 - Request that Entity
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
// 3 - Filter it if you want
//request.predicate = [NSPredicate predicateWithFormat:#"Person.name = Blah"];
// 4 - Sort it if you want
// First sort descriptor (required for grouping into sections):
NSSortDescriptor *sortByDate = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
// Second sort descriptor (for the items within each section):
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:#"firstname" ascending:YES];
[request setSortDescriptors:#[sortByDate, sortByName]];
//
// request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"firstname"
//ascending:YES
//selector:#selector(localizedCaseInsensitiveCompare:)]];
// 5 - Fetch it
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"sectionIdentifier"
cacheName:nil];
[self performFetch];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> theSection = [[self.fetchedResultsController sections] objectAtIndex:section];
NSString *sectionName = [theSection name];
if ([sectionName isEqualToString:#"0"]) {
return #"Today";
} else if ([sectionName isEqualToString:#"1"]) {
return #"Tomorrow";
}
else if ([sectionName isEqualToString:#"2"]) {
return #"Upcoming";
}
return #"Other";
}
- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return [[self.fetchedResultsController sections] count];
}
- (void) viewDidLoad
{
self.searchResults = [NSMutableArray arrayWithCapacity:[[self.fetchedResultsController fetchedObjects] count]];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setupFetchedResultsController];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Perform segue to detail when a SEARCH table cell is touched
if(tableView == self.searchDisplayController.searchResultsTableView)
{
[self performSegueWithIdentifier:#"Person Detail Segue" sender:tableView];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Persons Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
// Configure the cell...
Person *person = nil;
if (tableView == self.searchDisplayController.searchResultsTableView)
{
NSLog(#"Configuring cell to show search results");
person = [self.searchResults objectAtIndex:indexPath.row];
}
else
{
NSLog(#"Configuring cell to show normal data");
person = [self.fetchedResultsController objectAtIndexPath:indexPath];
}
NSString *fullname = [NSString stringWithFormat:#"%# %#", person.firstname, person.surname];
cell.textLabel.text = person.firstname;
if ([person.inRole.color isEqual :#"Yellow"])
{
cell.imageView.image = [UIImage imageNamed:#"Yellow"];
}
if ([person.inRole.color isEqual :#"Black"])
{
cell.imageView.image = [UIImage imageNamed:#"Black"];
}
if ([person.inRole.color isEqual :#"Grey"])
{
cell.imageView.image = [UIImage imageNamed:#"Grey"];
}
if ([person.inRole.color isEqual :#"Red"])
{
cell.imageView.image = [UIImage imageNamed:#"Red"];
}
if ([person.inRole.color isEqual :#"Blue"])
{
cell.imageView.image = [UIImage imageNamed:#"Blue"];
}
if ([person.inRole.color isEqual :#"Dark Green"])
{
cell.imageView.image = [UIImage imageNamed:#"DarkGreen"];
}
if ([person.inRole.color isEqual :#"Light Green"])
{
cell.imageView.image = [UIImage imageNamed:#"LightGreen"];
}
if ([person.inRole.color isEqual :#"Light Blue"])
{
cell.imageView.image = [UIImage imageNamed:#"LightBlue"];
}
if ([person.inRole.color isEqual :#"Brown"])
{
cell.imageView.image = [UIImage imageNamed:#"Brown"];
}
if ([person.inRole.color isEqual :#"Dark Orange"])
{
cell.imageView.image = [UIImage imageNamed:#"DarkOrange"];
}
NSDate *fechasinformat = person.date;
NSString *fecha0 = [NSString stringWithFormat:#"%#", fechasinformat];
cell.detailTextLabel.text = fecha0;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView)
{
return [self.searchResults count];
}
else
{
return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
}
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates]; // Avoid NSInternalInconsistencyException
// Delete the person object that was swiped
Person *personToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting (%#)", personToDelete.firstname);
[self.managedObjectContext deleteObject:personToDelete];
[self.managedObjectContext save:nil];
// Delete the (now empty) row on the table
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self performFetch];
[self.tableView endUpdates];
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Add Person Segue"])
{
NSLog(#"Setting PersonsTVC as a delegate of PersonDetailTVC");
PersonDetailTVC *personDetailTVC = segue.destinationViewController;
personDetailTVC.delegate = self;
NSLog(#"Creating a new person and passing it to PersonDetailTVC");
Person *newPerson = [NSEntityDescription insertNewObjectForEntityForName:#"Person"
inManagedObjectContext:self.managedObjectContext];
personDetailTVC.person = newPerson;
}
else if ([segue.identifier isEqualToString:#"Person Detail Segue"])
{
NSLog(#"Setting PersonsTVC as a delegate of PersonDetailTVC");
PersonDetailTVC *personDetailTVC = segue.destinationViewController;
personDetailTVC.delegate = self;
// Store selected Person in selectedPerson property
if(sender == self.searchDisplayController.searchResultsTableView)
{
NSIndexPath *indexPath = [self.searchDisplayController.searchResultsTableView indexPathForSelectedRow];
self.selectedPerson = [self.searchResults objectAtIndex:[indexPath row]];
}
else
{
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
self.selectedPerson = [self.fetchedResultsController objectAtIndexPath:indexPath];
}
NSLog(#"Passing selected person (%#) to PersonDetailTVC", self.selectedPerson.firstname);
personDetailTVC.person = self.selectedPerson;
}
else
{
NSLog(#"Unidentified Segue Attempted!");
}
}
- (void)theSaveButtonOnThePersonDetailTVCWasTapped:(PersonDetailTVC *)controller
{
// do something here like refreshing the table or whatever
// close the delegated view
[controller.navigationController popViewControllerAnimated:YES];
}
#pragma mark -
#pragma mark Content Filtering
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
self.searchResults = [[self.fetchedResultsController fetchedObjects] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
Person* person = evaluatedObject;
NSString* firstName = person.firstname;
//searchText having length < 3 should not be considered
if (!!searchText && [searchText length] < 3) {
return YES;
}
if ([scope isEqualToString:#"All"] || [firstName isEqualToString:scope]) {
return ([firstName rangeOfString:searchText].location != NSNotFound);
}
return NO; //if nothing matches
}]];
}
#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText:searchString scope:#"All"];
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
[self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:#"All"];
return YES;
}
#end
The error clearly indicates that you deleted 1 section, you had 1 before the deletion and you have 1 after the deletion. This is the problem. So probably your
- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
returns an incorrect number of sections after deleting last object.
Either your [[self.fetchedResultsController sections] count] is not 0 after deleting last object, or numberOfSectionsInTableView:(UITableView *)tableView is called by the self.searchDisplayController.searchResultsTableView and you don't check for that.
EDIT: To solve the issue add the following code:
- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
if(tableView==self.searchDisplayController.searchResultsTableView)
NSLog("search table asking");
else
NSLog("main table asking");
NSLog("fetched sections: %d", [[self.fetchedResultsController sections] count]);
return [[self.fetchedResultsController sections] count];
}
and post the console output when the crash occurs.
To implement the NSFetchedResultsController delegate methods add the code below to your viewController (I stripped some code out so check it compiles), and set the viewController as the delegate using something like _fetchedResultsController.delegate = self; when you create the controller (in setupFetchedResultsController).
Any time managedObjects that are in the fetchedResultsController result set are added, modified or deleted these methods are called. You can see that they include the necessary calls to the UITableView to update the display. These are used in conjunction with using the fetchedResultsController as the data source for the UITableView - that way you won't get any inconsistency errors.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"controllerWillChangeContent: called");
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{ NSLog(#"didChangeSection: called");
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
NSLog(#"controller:didChangeObject: called");
NSLog(#" object is %#", [anObject valueForKey:#"displayName"]);
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
{ NSLog(#"NSFetchedResultsChangeInsert: called");
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
{ NSLog(#"NSFetchedResultsChangeDelete: called");
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:
{ NSLog(#"NSFetchedResultsChangeUpdate: called");
[self configureCell:tableView cell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
break;
case NSFetchedResultsChangeMove:
{ NSLog(#"NSFetchedResultsChangeMove: called");
self.changedObjectIndex = newIndexPath;
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
// NOTE: This is overridden by subclasses to modify the default behaviour
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#“controllerDidChangeContent: called");
[self.tableView endUpdates];
}
- (void)configureCell:(UITableView *)tableView cell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
NSManagedObject *object;
object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [[object valueForKey:#"displayName"] description];
bool found =[[object valueForKey:#"found"] boolValue];
if (found) {
cell.textLabel.textColor = [UIColor greenColor];
}
}
A proper solution would be to make your PersonsTVC a delegate of the fetched results controller.
If you open an empty project that use CoreData you will have better understanding and probably find the solution to what you try to accomplish.
In its most simple form your code should be:
- (void)setupFetchedResultsController
{
//Setup your fetch request ...
NSFetchedResultsController* controller =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"sectionIdentifier"
cacheName:nil];
//setup the delegation and retain the FRC
controller.delegate = self;
self.fetchedResultsController = controller;
NSError* error = nil;
if (![controller performFetch:&error]) {
NSLog(#"error performing fetch: %#",error);
abort();
}
//Not sure what this does, seems redundant
//[self performFetch];
}
Now you must access your managed object through the FRC you created (not sure why you need self.searchResults).
As I said, in the simplest form, implement:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// In the simplest, most efficient, case, reload the table view.
[self.tableView reloadData];
}
Note: your deletion code should be:
This is also the reason your application crashed, as you updated the table before the FRC was able to report changes in the context.
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#", error);
abort();
}
}
}

NSFetchedResultsController W/ Custom Table Cell Incompatible Pointer

I've been trying to get a grip on CoreData. After several attempts to find a good tutorial on it I made a lot of progress with a tutorial on youtube. However when I got to the NSFetchedResultsController I'm getting a warning on my custom table cell ITDContactCell *cell = [tableView cellForRowAtIndexPath:indexPath] stating that it's an incompatible pointer initializing with UITableViewCell (the tutorial does not use custom cells).
Even worse, when I attempt to add an item I get this nice ugly error.
2013-12-04 19:15:48.407 Ping[490:60b] * Assertion failure in
-[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-2903.23/UITableView.m:1138 2013-12-04
19:15:48.409 Ping[490:60b] CoreData: error: Serious application error.
An exception was caught from the delegate of
NSFetchedResultsController during a call to
-controllerDidChangeContent:. attempt to insert row 0 into section 0, but there are only 0 rows in section 0 after the update with userInfo
(null)
I've spent hours trying to figure out what I'm doing wrong but I can't wrap my head around it. So if anyone can take a gander at my code and give me suggestions on how I can fix it I'd greatly appreciate it. Thank you.
Controller:
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.ContactsListTableView;
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
Contact *changeContact = [self.fetchedResultsController objectAtIndexPath:indexPath];
ITDContactCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.name.text = [NSString stringWithFormat:#"%# %#", changeContact.contactFirstName, changeContact.contactLastName];
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObjects:newIndexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
EDIT: My implementation of number of rows and sections.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections]count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections]objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
fetchedResultsController is defined in the interface simply as #property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController
The warning is referring to the fact that the method
[tableView cellForRowAtIndexPath:indexPath];
returns a UITableViewCell, which is not literally your ITDContactCell. Placing a cast before the method should remove the warning.
ITDContactCell *cell = (ITDContactCell*) [tableView cellForRowAtIndexPath:indexPath];
The other error is probably because of some lack of relationship between your fetchedResultsController and your tableView. Showing your tableView delegate and source methods would help. I assume you have already implemented something similar:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSError *error = nil;
if(![self.fetchedResultsController performFetch:&error]) {
// NSLog(#"Error! %#", error);
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return _fetchedResultsController.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> secInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [secInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
ITDContactCell *cell = (ITDContactCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// assign cell content
return cell;
}
-(NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[[self.fetchedResultsController sections] objectAtIndex:section] name];
}
-(NSFetchedResultsController *) fetchedResultsController {
if(_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityName"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.ContactsListTableView;
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
Contact *changeContact = [self.fetchedResultsController objectAtIndexPath:indexPath];
ITDContactCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.name.text = [NSString stringWithFormat:#"%# %#", changeContact.contactFirstName, changeContact.contactLastName];
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObjects:newIndexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
EDIT 1:
As a note, you should not be using the reloadData method in your tableview, since the table is reloaded (via the fetched results controller) after saving changes to your NSManagedObjectContext.
EDIT 2:
Is you NSManagedObjectContext nil?

Resources