What I'm trying to achive is UITableView reloading fast. Each cell has "checking" UIButton wchich tells user that item for this cell is selected. All selected cells should be on the end of the list (bottom cells).
I'm using NSFetchResultsController as a delegate and data source for UITableView. NSFetchResultsController is set to operate with "Item" entity with sectionKey selected for "checked" property (part od "Item" entity which )
- (id)initWithItemView:(ItemView*)iv{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:[CoreDataHandler context]];
[request setEntity:entity];
NSArray *predicates = [NSArray arrayWithObjects:[iv listDBUsingContext:[CoreDataHandler context]],nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"lista IN %# AND deletedDB == NO", predicates];
[request setPredicate:predicate];
[request setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"checked" ascending:YES selector:#selector(compare:)];
NSSortDescriptor *sortDescriptor2
= [[NSSortDescriptor alloc] initWithKey:#"position" ascending:YES selector:#selector(compare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, sortDescriptor2, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
[sortDescriptor2 release];
if (self=[[FastTableDelegate alloc]
initWithFetchRequest:request
managedObjectContext:[CoreDataHandler context]
sectionNameKeyPath:#"checked"
cacheName:nil])
{
self.delegate = self;
self.itemView = iv;
self.heightsCache = [NSMutableDictionary dictionary];
}
[request release];
[self performFetch:nil];
return self;
}
Then when the user tap UIButton in cell I change the "checked" property in NSmanagedObject (Item entity) and save the context. Then my NSFetchReslutsController is notified that one item changed it state and it performs UITableView reload using functions
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.itemView.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
}
Because I changed "checked" property which is sectionKey for my NSFetchedResultsController UITableView should be reloaded with this scenario
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
And here is a problem because when cells are inserted or delated UITableView performs animation which duration is about 0.5s. In this time UITableView stop reciving events for next cell so when the user "check" another cell it will not be notified about that tap. What I'm trying to achive is to enable user to quick checking some cells without waiting for animation to end.
Possible solutions:
Use reloadData istead of deleteRowsAtIndexPaths/insertRowsAtIndexPaths. It will be no animation but reloading all UITableView last longer that reloading one cell so in my case user will wait for UITableView to reload - again waiting. IS there a way to avoid animation without reloading all UITableView?
Enable user to check cells, but save context only after 0.5s after last cell selection. User can select fast multiple cell and when he ends selecting all changes are propagated to UITableView - my boss want to send each selected cell to second section without grouping then, one reload for each cell selected - again bad idea
Maybe some custom tableView (user created, not apple) ?
Primary goal:
Enable user to "check" cells quickly refreshing UITableView after each "checking"
I'm open for any ideals :)
Reload your table in -(void)viewDidAppear:(BOOL)animated
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[yourTable reloadData];
}
Related
I'm making an app which fetches a list of products from a server. I then store them in a Core Data database and I present them using GMGridView and the datasource is a NSFetchedResultsController. When I change the product details in the server, I want my iOS app to synchronize and make the necessary changes so I implement the NSFetchedResultsControllerDelegate method. How should I update my gridView properly?
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[_currentData insertObject:anObject atIndex:newIndexPath.row];
[_currentData removeObjectAtIndex:indexPath.row];
[_gmGridView insertObjectAtIndex:newIndexPath.row animated:YES];
[_gmGridView removeObjectAtIndex:indexPath.row animated:YES];
[_gmGridView reloadObjectAtIndex:newIndexPath.row animated:YES];
[_gmGridView reloadObjectAtIndex:indexPath.row animated:YES];
[_gmGridView reloadData];
//[self updatePageControl];
break;
case NSFetchedResultsChangeDelete:
[_currentData removeObjectAtIndex:indexPath.row];
[_gmGridView removeObjectAtIndex:indexPath.row animated:YES];
//[self updatePageControl];
[_gmGridView reloadInputViews];
[_gmGridView reloadData];
break;
case NSFetchedResultsChangeUpdate:
[_gmGridView reloadObjectAtIndex:indexPath.row animated:YES];
////////might be irrelevant, but just trying it out
[_currentData insertObject:anObject atIndex:newIndexPath.row];
[_currentData removeObjectAtIndex:indexPath.row];
[_gmGridView insertObjectAtIndex:newIndexPath.row animated:YES];
[_gmGridView removeObjectAtIndex:indexPath.row animated:YES];
[_gmGridView reloadObjectAtIndex:newIndexPath.row animated:YES];
[_gmGridView reloadObjectAtIndex:indexPath.row animated:YES];
////////
[_gmGridView reloadInputViews];
[_gmGridView reloadData];
break;
case NSFetchedResultsChangeMove:
[_currentData removeObjectAtIndex:indexPath.row];
[_currentData insertObject:anObject atIndex:newIndexPath.row];
[_gmGridView removeObjectAtIndex:indexPath.row animated:YES];
[_gmGridView insertObjectAtIndex:newIndexPath.row animated:YES];
[_gmGridView reloadInputViews];
[_gmGridView reloadData];
break;
}
}
Some details:
_currentData = [[self.fetchedResultsController fetchedObjects]mutableCopy];
//I did this because previously I wasn't using a fetchedResultsController but a NSMutableArray instead. I know that it's inefficient (because I have 2 models) but this is the simplest implementation I want to do now.
I make the CoreData changes in a different class by modifying the same UIManagedDocument alloc,init-ed from the same local URL.
However, I get 2 important problems:
The items in the database are updated (eg. change of product name or price) but the changes are not reflected in the UI, ie I can't reload my GMGridViewCell properly. (take note of the code related to reloading above)
Most of the time, the products I update in the server are duplicated in the database although I have a mechanism to prevent such an error. (Before I create a product, I first search for an existing one using a unique identifier. If there is an existing product, I just modify its details). Here's the code:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Product"];
request.predicate = [NSPredicate predicateWithFormat:#"product_id = %#", [imonggoInfo objectForKey:PRODUCT_ID]];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
request.sortDescriptors = [NSArray
arrayWithObject:sortDescriptor]; NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if (!matches | ([matches count] > 1)){
//handle error
}else if ([matches count] == 0){
//make a new product
}else{
//return existing
item = [matches lastObject];
}
Turns out I have to listen to NSManagedObjectContextDidSaveNotification and I have to merge the changes in the context modified in the background thread and the one in the main thread. I got the idea from the chosen answer in this question
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];
//Then the changes in the context has to be merged
- (void)contextDidSave:(NSNotification*)notification{
NSLog(#"contextDidSave Notification fired.");
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[self.itemDatabase.managedObjectContext performSelectorOnMainThread:selector withObject:notification waitUntilDone:NO];
}
Ok, I am getting better at this Core Data stuff, but I've got a ways to go. This is how I am populating my fetchedResultsController when my view loads:
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil)
{
return __fetchedResultsController;
}
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Visit" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Queue"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(isActive == YES)"];
[fetchedResultsController.fetchRequest setPredicate:predicate];
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return __fetchedResultsController;
}
This works great. I have the fetchedResultsController hooked up to my tableView and I have 5 managed objects that are showing up. The problem I am running into is when I need to make a change to one of the managed objects.
As you can see in the predicate I am specifying that I only want managed objects that have isActive == YES. I need to change a managed object's isActive status to NO, and then remove it from the fetchedResultsController, and ultimately the tableView.
Here is how I am trying to do that:
-(void) seatedButton{
Visit * vis = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:reloadIndex inSection:0]];
vis.isActive = [NSNumber numberWithBool:NO];
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[tableView reloadData];
}
It appears that this would work, but it doesn't. When the tableView is reloaded, 5 objects come back as fitting the requirements of the predicate, when it should only be 4!
What am I doing wrong here? What do I need to do to update the fetchedResultsController? Thanks!
There are delegate methods to the fetchedResultsController you may want to use that should do this all for you automatically. Once you save the context, it update the table for you, so you won't have to call [tableView reloadData]
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (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;
}
}
- (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:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
EDIT: If you do this, you will need to implement the following method:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// configure cell;
}
I'm pretty sure adding the following code into your seatedButton method, just before the line with [tableView reloadData] will fix the problem, but not sure according to the doc that it should be necessary.
if (![self.fetchedResultsController performFetch:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
I am getting the following error when loading one of my views that has a UITableView in it. Does anyone know how to fix it? I have already tried deleting the (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath method, but that didn't help.
I'm thinking it has to do with the table's delegate not being updated properly with numberOfRowsInSection or something.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-1448.89/UITableView.m:995
2011-06-05 00:38:12.116 App[14523:707] 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 (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 3 deleted). with userInfo (null)
Here is my code:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete the managed object for the given index path
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
NSLog(#"fetched results : \n%#\n",[self.fetchedResultsController fetchedObjects]);
// Commit the change.
NSError *error = nil;
// Update the array and table view.
if (![managedObjectContext save:&error])
{
// Handle the error.
}
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}
#pragma mark - Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil)
{
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Set" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat: #"sets == %#", self.exercise]];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
NSLog(#"fetched results : \n%#\n",[self.fetchedResultsController fetchedObjects]);
NSLog(#"fetch count: %i", [fetchedResultsController.fetchedObjects count]);
return self.fetchedResultsController;
}
#pragma mark - Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.setsTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.setsTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.setsTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.setsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.setsTableView endUpdates];
[self.setsTableView reloadData];
}
is Try uncommenting [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
Please note:
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
should return a value 1 less than that of before calling 'deleteRows..'
Basic problem is this: You are instructing UI to delete one of the rows, but not removing that row from the back end.
If you are using the code from CoreDataBooks sample you might want to change this:
- (void)addControllerContextDidSave:(NSNotification*)saveNotification {
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
}
with
- (void)addControllerContextDidSave:(NSNotification*)saveNotification {
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
});
}
This will prevent NSFetchedResultsController from refreshing while the tableView is not visible because of the UINavigationController transition.
Of course this thing occurs only in simulator, because on the device the insert will take longer than the [self dismissModalViewControllerAnimated:YES] animation.
In my case when I called [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES]; only, I got this type of error. I solved this by removing the object from Array (from this array I am displaying data.) and called [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
I put each section elements in separated arrays. Then put them into another array( arrayWithArray). My solution here for this problem:
[quarantineMessages removeObject : message];
[_tableView beginUpdates];
if([[arrayWithArray objectAtIndex: indPath.section] count] > 1)
{
[_tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indPath] withRowAnimation:UITableViewRowAnimationBottom];
}
else
{
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:indPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
[_tableView endUpdates];
I have the following method which is adding a cell to a tableview. I want the added cell to be at the bottom, however right now it is adding it at the top. Any suggestions?
addEvent method:
-(void)addEvent
{
Routine *routine = (Routine *)[NSEntityDescription insertNewObjectForEntityForName:#"Routine" inManagedObjectContext:managedObjectContext];
routine.name=entered;
NSError *error = nil;
if (![managedObjectContext save:&error]) {
// Handle the error.
}
NSLog(#"%#", error);
[eventsArray insertObject:routine atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.routineTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.routineTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
ViewDidLoad
- (void)viewDidLoad
{
if (managedObjectContext == nil)
{
managedObjectContext = [(CurlAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Routine" inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
[self setEventsArray:mutableFetchResults];
[mutableFetchResults release];
[request release];
UIBarButtonItem * addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(showPrompt)];
[self.navigationItem setLeftBarButtonItem:addButton];
[addButton release];
UIBarButtonItem *editButton = [[UIBarButtonItem alloc]initWithTitle:#"Edit" style:UIBarButtonItemStyleBordered target:self action:#selector(toggleEdit)];
self.navigationItem.rightBarButtonItem = editButton;
[editButton release];
[super viewDidLoad];
}
0 is the first index. As a rule (there are likely to be a few exceptions somewhere in the world) things that are to do with indexes, start at 0. Whereas things like count start at 1.
So if you have an array with 1 object in it, the array's count will be 1, and the object will be at index 0.
When you are using 0, for you indexPath's row and section your telling it to put it at the top of the tableview.
So make you last 4 lines of code something like this:
[eventsArray addObject:routine];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.routineTableView reloadData];
NSInteger lastSection = [self.routineTableView numberOfSections] -1;
[self.routineTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.routineTableView numberOfRowsInSection:lastSection]-1 inSection:lastSection] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Probably use [eventsArray addObject:] instead of insertObject:atIndex:
Also, you should be able to use [self.routineTableView reloadData]; rather than inserting rows manually -- assuming you've set up the table view controller in a normal way.
The specific answer to your question is that your table has an array as a datasource and you are adding a new item at the beginning of the array. Therefore, when the table is reloaded, the new cell is at the top.
For a deeper understanding, I recommend you understand the following topics at minimum:
common data types used as data sources with tables (arrays and dictionaries mainly)
how table views work (e.g. what is tableView.dataSource and tableView.delegate)
methods of reloading the table when the dataSource changes (what you did is ok but not always what you'll want)
When the user presses Edit, my UITableView adds an insert row at the top (with a green plus), and puts all the other rows into delete mode (red minus). Alternatively, the user can swipe-to-delete without pressing the edit button. I am using a couple of Ivars to keep track of whether the table is in edit mode from a swipe, or from pressing the edit button, and act accordingly (e.g. updating numberOfRowsInTableView: with the extra insert row when Edit has been pressed).
Everything works perfectly except on thing: when in Edit mode (i.e. the user has explicitly hit the edit button, and the insert row has appeared at the top), if the user tries to delete the bottom row, the next row up gets deleted instead. Deleting any other row is fine.
EDIT -- It appears to delete the row above, but if I immediately quit and reload the app, it turns out the bottom row has gone after all. So I'm guessing my UITableView is going out of sync with my NSFetchedResultsController somewhere.
Here's the code I'm using:
#import "ChecklistsViewController.h"
#import "Checklist.h"
#interface ChecklistsViewController (private)
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
- (void)addingView;
#end
#implementation ChecklistsViewController
#synthesize category, managedObjectContext, fetchedResultsController;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
editingFromSwipe = NO;
tableIsEditing = NO;
}
return self;
}
- (void)dealloc
{
[category release];
[managedObjectContext release];
[fetchedResultsController release];
[super dealloc];
}
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
editingFromSwipe = NO;
tableIsEditing = NO;
self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.tableView.allowsSelectionDuringEditing = YES;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#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];
int rows = [sectionInfo numberOfObjects];
if (self.editing) {
if (!editingFromSwipe && tableIsEditing) {
return rows +1;
}
return rows;
}
tableIsEditing = NO;
return rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
NSLog(#"Should go into if statement here! \n");
if (tableView.editing) { //
if ((indexPath.row == 0) && (!editingFromSwipe)) {
NSLog(#"Configuring Add Button Cell while editing \n");
cell.textLabel.text = #"Add New Checklist";
cell.detailTextLabel.text = nil;
}
else {
NSLog(#"Configuring other cells while editing \n");
[self configureCell:cell atIndexPath:indexPath];
}
}
else {
NSLog(#"Configuring Cell Normally While Not Editing \n");
[self configureCell:cell atIndexPath:indexPath];
}
return cell;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete the managed object for the given index path
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
int numberOfRows = [self tableView:tableView numberOfRowsInSection:indexPath.section];
int rowBeingDeleted = indexPath.row +1;
if (tableIsEditing && !editingFromSwipe && numberOfRows == rowBeingDeleted) {
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section]]];
}
else {
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
// TO DO: Fix error code.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
[self addingView];
}
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
int row = indexPath.row;
if (self.editing && row == 0) {
if (!editingFromSwipe && tableIsEditing) {
return UITableViewCellEditingStyleInsert;
}
else if (editingFromSwipe) {
return UITableViewCellEditingStyleDelete;
}
}
return UITableViewCellEditingStyleDelete;
}
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
editingFromSwipe = YES;
[super tableView:tableView willBeginEditingRowAtIndexPath:indexPath];
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
[super tableView:tableView didEndEditingRowAtIndexPath:indexPath];
editingFromSwipe = NO;
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
NSArray *addRow = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:0 inSection:0], nil];
[self.tableView beginUpdates];
if (!editingFromSwipe) {
if (editing) {
tableIsEditing = YES;
[self.tableView insertRowsAtIndexPaths:addRow withRowAnimation:UITableViewRowAnimationLeft];
}
else {
[self.tableView deleteRowsAtIndexPaths:addRow withRowAnimation:UITableViewRowAnimationLeft];
}
}
[self.tableView endUpdates];
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row != 0) {
TO DO: Code for when row is selected
}
}
#pragma mark - Data
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Checklist *aChecklist = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = aChecklist.name;
cell.detailTextLabel.text = aChecklist.category.name;
}
- (void) addingView// :(id)sender
{
AddingViewController *viewController = [[AddingViewController alloc] initWithNibName:#"AddingViewController" bundle:nil];
viewController.delegate = self;
viewController.title = #"Add Checklist";
// Create the navigation controller and present it modally
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[self presentModalViewController:navigationController animated:YES];
viewController.textLabel.text = #"Enter new checklist name";
[navigationController release];
[viewController release];
}
#pragma mark - AddingViewDelegate
- (void)addingViewController:(AddingViewController *)addingViewController didAdd:(NSString *)itemAdded
{
if (itemAdded != nil) {
// Turn off editing mode.
if (self.editing) [self.navigationController setEditing:NO animated:NO];
// Add the category name to our model and table view.
// Create a new instance of the entity managed by the fetched results controller.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
Checklist *newChecklist = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[category addChecklistsObject:newChecklist];
newChecklist.name = itemAdded;
// [newChecklist setDateStamp:[NSDate date]];
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
TO DO: fix error code.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
[self dismissModalViewControllerAnimated:YES];
}
#pragma mark - Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil)
{
return fetchedResultsController;
}
// Set up the fetched results controller.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Checklist" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set 4* the predicate so we only see checklists for this category.
NSPredicate *requestPredicate = [NSPredicate predicateWithFormat:#"category.name = %#", self.category.name];
[fetchRequest setPredicate:requestPredicate];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
// TO DO: error stuff
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
}
#pragma mark - Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (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;
}
}
- (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:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
#end
You can add static cells to UITableViews that get their data from a NSFetchedResultsController. But to do this you have to adjust almost all NSIndexPaths that are used within one of the UITableViewDelegate, UITableViewDataSource or NSFetchedResultsControllerDelegate methods.
I added some helper methods that translate the indexpath of the tableview to the indexpath of the fetched resultscontroller and the other way around. Something like this could be used if you want to add a row on top:
- (NSIndexPath *)tableIndexPathFromNSFRCIndexPath:(NSIndexPath *)ip {
if (editingMode && ip.section == 0) {
NSIndexPath *newIP = [NSIndexPath indexPathForRow:ip.row+1 inSection:ip.section];
return newIP;
}
return ip;
}
- (NSIndexPath *)nsfrcIndexPathFromTableIndexPath:(NSIndexPath *)ip {
if (editingMode && ip.section == 0) {
NSIndexPath *newIP = [NSIndexPath indexPathForRow:ip.row-1 inSection:ip.section];
return newIP;
}
return ip;
}
and then you have to change every method that passes an indexpath from the table to the fetchedresultscontroller or from the frc to the table. I show you two as an example.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
newIndexPath = [self tableIndexPathFromNSFRCIndexPath:newIndexPath];
indexPath = [self tableIndexPathFromNSFRCIndexPath:indexPath];
switch(type) {
case NSFetchedResultsChangeInsert:
[self.listTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.listTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.listTableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.listTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.listTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[aTableView deselectRowAtIndexPath:indexPath animated:YES];
if (editingMode && indexPath.section == 0 && indexPath.row == 0) {
// Add New entry...
}
else {
indexPath = [self nsfrcIndexPathFromTableIndexPath:indexPath];
NSManagedObject *selectedObject = [self.fetchedResultsController objectAtIndexPath:indexPath]);
}
}
Your design it at fault. You shouldn't be adding rows anywhere but where they logically go in the table.
The entire point of a fetched results controller (FRC) is to synchronize the tableview with the data. The order of the rows in the table should reflect the order of managed objects in the fetchedObjects array. By inserting a row at the bottom or top and while adding an object that does not necessarily logically belong at the top or bottom of the table, are breaking that synchronization.
When you add a new managed object in addingViewController:didAdd: the FRC alerts it delegate which tries to redraw the table. You've tried to compensate for this but you really can't. All you indexes are coming off.
Instead of using a row to input new rows. Use a tableview header or footer view. That way, you can freeze the tableview, create the new object, then update the table and the new object will show up where it logical belongs in the table.
To not get the rows mixed up I would suggest putting the insert row in its own section. Since you're obviously just using one section you know that the section you send to FRC should always be 0. The code would be as simple as:
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:0]]];
TechZen's solution will also work, so which solution you choose is entirely about what design you prefer. TechZen's solution doesn't interfere with having multiple sections, but this solution could be modified to support multiple sections as well.