moveRows with NSFetchedResultsController bug - ios

I have a bug in here somewhere and I can not find it so I am hoping your keen eyes will!
I am using a FRC with a tableView. the FRC is section sorted by keyPath and then sorted by "displayOrder" - the usual.
The Details "displayOrder" in each section start at 1 so when I insert an item, in another method, it goes to index 0 of the section.
I want to loop through the affected section(s) and re-assign the "displayOrder" starting at 1.
During re-order, the code works for:
Re-ordering within the any section AS LONG AS the re-ordered cell moves up and not down.
Code does not work for... clicking on a cell but not moving it.. the code changes the order for some reason thus changing the order of the cells. - when I click a cell, it along with the other cells above it in the same section re-order.
I used to have this working and I don't know what happened.
Thanks for any help.
-Edited-
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
NSError *error = nil;
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
TheDetail *fromThing = [self.fetchedResultsController objectAtIndexPath:fromIndexPath];
TheDetail *toThing = [self.fetchedResultsController objectAtIndexPath:toIndexPath];
NSPredicate *catetgoryPredicate = [NSPredicate predicateWithFormat:#"relationshipToTheCategory.name == %#", fromThing.relationshipToTheCategory.name];
NSMutableArray *allThings = [[[self.fetchedResultsController fetchedObjects] filteredArrayUsingPredicate:catetgoryPredicate] mutableCopy];
NSPredicate *fromPredicate = [NSPredicate predicateWithFormat:#"relationshipToTheSection.name == %#", fromThing.relationshipToTheSection.name];
NSPredicate *toPredicate = [NSPredicate predicateWithFormat:#"relationshipToTheSection.name == %#", toThing.relationshipToTheSection.name];
[allThings removeObject:fromThing];
[allThings insertObject:fromThing atIndex:toIndexPath.row];
//if the sections are NOT the same, reorder by section otherwise reorder the one section
if (![fromThing.relationshipToTheSection.name isEqual:toThing.relationshipToTheSection.name]) {
//Change the from index section's relationship and save, then grab all objects in sections and re-order
[fromThing setRelationshipToTheSection:toThing.relationshipToTheSection];
if ([context save:&error]) {
NSLog(#"The setting section save was successful!");
} else {
NSLog(#"The setting section save was not successful: %#", [error localizedDescription]);
}
NSMutableArray *fromThings = [[allThings filteredArrayUsingPredicate:fromPredicate]mutableCopy];
NSInteger i = 1;
for (TheDetail *fromD in fromThings) {
[fromD setValue:[NSNumber numberWithInteger:i] forKey:#"displayOrder"];
i++;
}
//reset displayOrder Count, the re-order the other section
i = 1;
NSMutableArray *toThings = [[allThings filteredArrayUsingPredicate:toPredicate]mutableCopy];
for (TheDetail *toD in toThings) {
[toD setValue:[NSNumber numberWithInteger:i] forKey:#"displayOrder"];
i++;
}
} else {
NSMutableArray *fromThings = [[allThings filteredArrayUsingPredicate:fromPredicate]mutableCopy];
NSInteger i = 1;
for (TheDetail *fromD in fromThings) {
[fromD setValue:[NSNumber numberWithInteger:i] forKey:#"displayOrder"];
i++;
}
}
if ([context save:&error]) {
NSLog(#"The save was successful!");
} else {
NSLog(#"The save was not successful: %#", [error localizedDescription]);
}
FRC
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
NSManagedObjectContext *context = [[self appDelegate]managedObjectContext];
//Construct the fetchResquest
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
NSEntityDescription *detail = [NSEntityDescription entityForName:#"TheDetail" inManagedObjectContext:context];
[fetchRequest setEntity:detail];
//Add predicate
NSString *category = #"1";
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"relationshipToTheCategory == %#", category];
[fetchRequest setPredicate:predicate];
//Add sort descriptor
NSSortDescriptor *sortDescriptor2 = [NSSortDescriptor sortDescriptorWithKey:#"relationshipToTheSection.displayOrder" ascending:YES];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc]initWithObjects:sortDescriptor2, sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
//Set fetchedResultsController
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"relationshipToTheSection.name" cacheName:#"Root"];
NSError *error = nil;
self.fetchedResultsController = theFetchedResultsController;
self.fetchedResultsController.delegate = self;
[self.fetchedResultsController performFetch:&error];
return _fetchedResultsController;
New Error
Section *toSection = [[self fetchedResultsController] sections][[toIndexPath section]];
NSString *toSectionName = [[[toSection objects] lastObject] name];
Here I get the error in the IB "No visible #interface for "DSection" declares the selector 'objects'.

Don't remove yourself as the delegate for the NSFetchedResultsController. That is against the intended design of that class. If that is "helping" then it is masking a real problem.
Don't call -performFetch; from this method. The NSFetchedResultsController will detect the changes and tell your delegate about them.
Don't call -reloadData from this method. Let the delegate methods of NSFetchedResultsController do the reordering.
Always, always, always capture the error on a core data save. Even though you really don't need to save here (this is a bad time to block the UI with a save), you should ALWAYS capture the error and then watch for the result otherwise errors are hidden.
It is not clear what the -save: is doing. You haven't changed anything by the point of that save.
So that is a lot of work you are doing that you don't need to do. You are fighting the framework and making things harder.
Your reordering logic is more complicated than it needs to be, I think. It would help to see the NSFetchedResultsController initialization as well. But I am guessing you have sections based on name and then order by displayOrder. If that is the case this code can be a lot cleaner which would then make the issue more apparent.
My question to you is, are you checking this with breakpoints? Is this code firing when a row doesn't get actually moved? Should you check to see if your toIndexPath and fromIndexPath are equal?
Update
You do not need to save your context here. This is a UI method, saving causes delays which will make the UI slow to respond. Save later.
You do not need to run a NSFetchRequest here. That also hits disk and causes delays in the UI. Every piece of information that you need is already in memory inside of your NSFetchedResultsController. Use the existing object relationships to retrieve the data you are needing to make your decisions.
Calling entities The* is against Objective-C naming conventions. Words like "the", "is", "are" do not belong in entity or class names.
Consider this version of your code:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext];
TheDetail *fromThing = [[self fetchedResultsController] objectAtIndexPath:fromIndexPath];
Section *toSection = [[self fetchedResultsController] sections][[toIndexPath section]];
NSString *toSectionName = [[[toSection objects] lastObject] name];
NSString *fromSectionName = [[fromThing relationshipToTheSection] name];
if ([toSectionName isEqualToString:fromSectionName]) {
//Same section, easy reorder
//Move the object
NSMutableArray *sectionObjects = [[[[self fetchedResultsController] sections][[fromIndexPath section]] objects] mutableCopy];
[sectionObjects removeObject:fromThing];
[sectionObjects insertObject:fromThing atIndex:[toIndexPath row]];
//Reorder
NSInteger index = 1;
for (TheDetail *thing in sectionObjects) {
[thing setValue:#(index) forKey:#"displayOrder"];
}
return; //Early return to keep code on the left margin
}
NSMutableArray *sectionObjects = [[[[self fetchedResultsController] sections][[fromIndexPath section]] objects] mutableCopy];
[sectionObjects removeObject:fromThing];
//Reorder
NSInteger index = 1;
for (TheDetail *thing in sectionObjects) {
[thing setValue:#(index) forKey:#"displayOrder"];
}
if ([[toSection numberOfObjects] count] == 0) {
[fromThing setValue:#(0) forKey:#"displayOrder"];
//How do you determine the name?
return;
}
sectionObjects = [[toSection objects] mutableCopy];
[sectionObjects insertObject:fromThing atIndex:[toIndexPath row]];
//Reorder
NSInteger index = 1;
for (TheDetail *thing in sectionObjects) {
[thing setValue:#(index) forKey:#"displayOrder"];
}
}
There is no fetching and no saving. We are working with only what is in memory already so it is VERY fast. This should be C&P-able except for one of the comments I left in.

Related

how to change the index of the CoreData when reordering the TableView?

Please go through the code and tell me what i am doing wrong here.
TableViewController.m
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Device" inManagedObjectContext:context];
NSFetchRequest *fetchrequest = [[NSFetchRequest alloc]initWithEntityName:entity];
_devices = [[context executeFetchRequest:fetchrequest error:nil]mutableCopy];
[self.tableView reloadData]; // When TableView is reloading the data in tableview updating according to the data stored via CoreData.
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
if (fromIndexPath != toIndexPath ) {
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *device = [self.devices objectAtIndex:fromIndexPath.row];
NSString *str1 = [device valueForKey:#"text1"];
NSLog(#"%#",str1);
[self.devices removeObject:device];
[self.devices insertObject:device atIndex:[toIndexPath row]];
// int i = 0;
// for (NSManagedObject *mo in self.devices)
// {
// NSManagedObject *new = [self.devices objectAtIndex:i];
// NSString *check = [new valueForKey:#"text1"];
// NSLog(#"%#",check);
// NSString *check1 = [new valueForKey:#"text2"];
// NSLog(#"%#",check1);
// NSString *check2 = [new valueForKey:#"text3"];
// NSLog(#"%#",check2);
// [mo setValue:[NSString stringWithFormat:#"%#",[new valueForKey:#"text1"]] forKey:#"text1"];
// [mo setValue:[NSString stringWithFormat:#"%#",[new valueForKey:#"text2"]] forKey:#"text2"];
// [mo setValue:[NSString stringWithFormat:#"%#",[new valueForKey:#"text3"]] forKey:#"text3"];
// i++;
// }
NSError *error;
if(![context save:&error]){
NSLog(#"%# %#",error, [error localizedDescription]);
}
}
}
I have entity Name: Device and with column names: text1,text2,text3
I don't want to sort the data in Core Data, actually i want to move the rows in TableView and i want to update the index of the data saved in columns text1,text2,text3. In total, there are 5 rows in tableview and i just want to update the index of the values with respect to the tableview index which is updated.
Check below tutorial
http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/
for short you need to add "displayIndex" property to the objects you are trying to reoder

How to change the order of objects in the array of fetchresultcontroller after reordering the cells

I have a table view and I just implemented a class that helps me reorder the cells, like the regular moving cells method that comes with the table view delegate.
Now after I reorder the cells, I need to change the array that holds the cell objects to the new order... How do I do that?
This is my method for reordering the cells:
- (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { NSArray
}
i have a coreDataStack class that takes care of all the core data stuff (creating a singelton), it looks like this:
#import "CoreDataStack.h"
#implementation CoreDataStack
#pragma mark - Core Data stack
#synthesize managedObjectContext = _managedObjectContext;
#synthesize managedObjectModel = _managedObjectModel;
#synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
+ (instancetype)defaultStack {
static CoreDataStack *defaultStack;
static dispatch_once_t onceTocken;
dispatch_once (&onceTocken, ^{
defaultStack = [[self alloc] init];
});
return defaultStack;
}
- (NSURL *)applicationDocumentsDirectory {
// The directory the application uses to store the Core Data store file. This code uses a directory named "digitalCrown.Lister" in the application's documents directory.
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
- (NSManagedObjectModel *)managedObjectModel {
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Lister" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it.
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
// Create the coordinator and store
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Lister.sqlite"];
NSError *error = nil;
NSString *failureReason = #"There was an error creating or loading the application's saved data.";
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
// Report any error we got.
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSLocalizedDescriptionKey] = #"Failed to initialize the application's saved data";
dict[NSLocalizedFailureReasonErrorKey] = failureReason;
dict[NSUnderlyingErrorKey] = error;
error = [NSError errorWithDomain:#"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
// Replace this 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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
#pragma mark - Core Data Saving support
- (void)saveContext {
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
NSError *error = nil;
if ([managedObjectContext hasChanges] && ![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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
#end
and whenever i add new object to core data i do it this way:
- (void)insertTeget {
CoreDataStack *stack = [CoreDataStack defaultStack];
Target *target = [NSEntityDescription insertNewObjectForEntityForName:#"Target" inManagedObjectContext:stack.managedObjectContext];
if (self.myTextView.text != nil) {
target.body = self.myTextView.text;
target.time = [NSDate date];
}
[stack saveContext];
}
in the table view when I'm fetching the data i do it this way:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"StackTableViewCell";
Target *target = [self.fetchedResultController objectAtIndexPath:indexPath];
StackTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"StackTableViewCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
cell.cellLabel.text = target.body;
cell.cellLabel.font = [UIFont fontWithName:#"Candara-Bold" size:20];
cell.showsReorderControl = YES;
// Configure the cell...
return cell;
}
this is my fetchresultconroller/fetch request config in the table view controller class:
- (NSFetchRequest *)targetsFetchRequest {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Target"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"time" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
return fetchRequest;
}
- (NSFetchedResultsController *)fetchedResultController {
if (_fetchedResultController != nil) {
return _fetchedResultController;
}
CoreDataStack *stack = [CoreDataStack defaultStack];
NSFetchRequest *fetchRequest = [self targetsFetchRequest];
_fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:stack.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
_fetchedResultController.delegate = self;
return _fetchedResultController;
}
What I want to accomplish is whenever a user create a target object it will go to the end of the array (so it will be like a queue), and if a user move cells, so I need to change the order of array of the database...
moving cells method:
- (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
int start = 0;
int end = 0;
if (fromIndexPath.row > toIndexPath.row) {
start = (int)fromIndexPath.row;
end = (int)toIndexPath.row;
} else {
start = (int)toIndexPath.row;
end = (int)fromIndexPath.row;
}
for (int i = start; i <= end; ++i) {
Target *target = [self.fetchedResultController objectAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[target setOrder:#(i)];
}
[[CoreDataStack defaultStack] saveContext];
// a test to see if the order is changed
[self.fetchedResultController performFetch:nil];
NSArray *arr = [self.fetchedResultController fetchedObjects];
for (int i=0; i<arr.count; i++) {
Target *ta = [arr objectAtIndex:i];
NSLog(#"%#",ta.body);
}
}
the log:
2015-04-14 10:29:13.405 Lister[3163:477453] One
2015-04-14 10:29:13.406 Lister[3163:477453] Two
2015-04-14 10:29:13.406 Lister[3163:477453] Three
2015-04-14 10:29:13.407 Lister[3163:477453] Four
2015-04-14 10:29:13.407 Lister[3163:477453] Five
2015-04-14 10:29:21.070 Lister[3163:477453]
2015-04-14 10:29:21.071 Lister[3163:477453] One
2015-04-14 10:29:21.071 Lister[3163:477453] Two
2015-04-14 10:29:21.071 Lister[3163:477453] Three
2015-04-14 10:29:21.072 Lister[3163:477453] Four
2015-04-14 10:29:21.072 Lister[3163:477453] Five
2015-04-14 10:29:25.037 Lister[3163:477453]
2015-04-14 10:29:25.039 Lister[3163:477453] One
2015-04-14 10:29:25.039 Lister[3163:477453] Two
2015-04-14 10:29:25.040 Lister[3163:477453] Three
2015-04-14 10:29:25.040 Lister[3163:477453] Four
2015-04-14 10:29:25.041 Lister[3163:477453] Five
Also , the label of the cells is acting weird now, if move the cell with the label "one" to the index of the cell with label "two", so the label of "one" is changing to "two". So i get to the situation that 2 cells have the same label.
Well the simplest solution would be
Add an attribute to your Target entity, say it order of type Integer32.
Creating and Inserting New Objects
Whenever you create a new Target object, first fetch the existing objects from the database using sortDescriptor having key #"order" and ascending=YES. Take the last object of this fetched array and check its order. Now in your new Target object increment the order and insert it to the database. If the fetched array returns 0 objects, then set order=#(0).
- (void)insertTeget {
CoreDataStack *stack = [CoreDataStack defaultStack];
//Fetching objects from database
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Target"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"order" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
NSArray *existingObjects = [stack.managedObjectContext executeFetchRequest:fetchRequest error:nil];
//Creating new object
Target *target = [NSEntityDescription insertNewObjectForEntityForName:#"Target" inManagedObjectContext:stack.managedObjectContext];
if (self.myTextView.text != nil) {
target.body = self.myTextView.text;
target.order = #([(Target *)existingObjects.lastObject order].integerValue + 1);
}
[stack saveContext];
}
NSFetchedResultsController
Fetch the objects using the above defined sortDescriptor.
Taken from your code
- (NSFetchRequest *)targetsFetchRequest {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Target"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"order" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
return fetchRequest;
}
- (NSFetchedResultsController *)fetchedResultController {
if (_fetchedResultController != nil) {
return _fetchedResultController;
}
CoreDataStack *stack = [CoreDataStack defaultStack];
NSFetchRequest *fetchRequest = [self targetsFetchRequest];
_fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:stack.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
_fetchedResultController.delegate = self;
return _fetchedResultController;
}
Rearranging cells
Now while when you rearrange cells in your table view, you just need to run a for loop and update their order. You need to only update order of objects between the two indexPaths.
- (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
int start = 0;
int end = 0;
if (fromIndexPath.row > toIndexPath.row) {
start = fromIndexPath.row;
end = toIndexPath.row;
} else {
start = toIndexPath.row;
end = fromIndexPath.row;
}
for (int i = start; i <= end; ++i) {
Target *target = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[target setOrder:#(i)];
}
[[CoreDataStack defaultStack] saveContext];
}
Note: The above solution assumes that you have order beginning from 0.
When you create and insert new Target objects you need to implement NSFetchedResultsController delegate methods to add corresponding rows for those objects. Since we have already defined sortDescriptor, the new rows will be added at the end of the tableView.
- (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;
}
}
- (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 NSFetchedResultsChangeUpdate:
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];
}
Simple Solution: Reffer Apple's Doc:
Create a NSMutableArray to identify reordered array.
Step 1: Declare a NSMutableArray property #property (strong, nonatomic) NSMutableArray *arrayTag in header or class file.
Step 2: Initialise in viewDidLoad
Step 3: Add this code in tableview delegate methods
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
NSString *stringToMove = [arrayTag objectAtIndex:sourceIndexPath.row];
[arrayTag removeObjectAtIndex:sourceIndexPath.row];
[arrayTag insertObject:stringToMove atIndex:destinationIndexPath.row];
}
try this
-(void)moveTableView:(UITableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSString *str1 = [[yourArray objectAtIndex:fromIndexPath.row] copy];
NSString *str2 = [[yourArray objectAtIndex:toIndexPath.row] copy];
[yourArray replaceObjectAtIndex:fromIndexPath.row withObject:str2];
[yourArray replaceObjectAtIndex:toIndexPath.row withObject:str1];
[tableView reloadData];
}
If I understood your Q correctly, you have to change the schema of the model.
A. First, what I understood
You have a list of items. New items are added at the end. Now you want to give the user the ability to reorder the items in a customized manner.
B. What you do
Actually you are using a creation date attribute for ordering. Of course you cannot use this for reordering, because this would mean to change the creation date. Therefore the whole approach using a creation date fails: It is good enough for a list not changed by the user, but not, if you have a customized order.
C. What you can do
If you have a customized order, you need a customized attribute to reflect the order. If the list is a property of an instance object, you can use NSOrderedSet and Core Data's ordered relationship for doing so. I wouldn't because of pay-offs. However, you can do it, if works for you.
Otherwise you have to handle that yourself:
a. Add an attribute order to your entity type.
b. When you insert a new object, check for the count of the existing list (depends on how you hold it) and set the value as the order property of the new instance.
c. When fetching, use that property for sorting.
d. When changing, change the attribute of the instance object and of all instance objects in between source and destination. Let me explain that:
We have a lis like that:
name order
Amin 0
Negm 1
Awad 2
Answer 3
Now, for example, Answer is moved upwards from position 3 to position 1 (ahead of Negm):
name order
Amin 0
Answer 3
Negm 1
Awad 2
That means that the order attribute of the moved object (3) has to be changed to the new destination (1) and of all objects having an order attribute of >=1 to <3 has to be changed to +1. (And the order attribute of the moved object, too, obviously)
name order
Amin 0
Answer 1
Negm 2
Awad 3
In Code
NSUInteger oldIndex = …; // 3
NSUInteger newIndex = …; // 1
movedObject.order = newIndex;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Target"];
NSPredicate *betweenPredicate = [NSPredicate predicateWithFormat:#"order >= %ld AND order < %ld", newIndex, oldIndex];
NSArray *objectsToChange = [context executeFetchRequest:request error:NULL];
for( Target *target in objectsToChange )
{
target.order = #([target.order unsignedIntegerValue] + 1);
}
If an item is moved down, you have to do the same the other way round.
If you have different lists of unique objects like playlists in iTunes, you need an extra entity type instead of an extra attribute. Let me know that, I will post the code from one of my books including moving a gapped list of items.
You have to change the underlying data model to reflect the new order if you want NSFetchResultsController to pick up the change.

UILabel in custom UITableView cell not updating with Core Data change

I'm attempting to build a game scoring app that utilizes a custom table cell with player photos, names, buttons etc... There are add/subtract buttons directly in the custom cell of the tableview that are hitting my save method, and it's storing it back in Core Data for that specific user.
The problem is with the on-screen score not updating and reflecting the change. After the save action to Core Data is complete, I'm calling the [self.tableView reloadData];... nothing. However, if I restart the app, then the change in score (for any of the players I've clicked on), appears.
Maybe I'm making this harder than it needs to be, either that, or I'm just not grasping the real problem.
Thoughts / comments?
Thanks a load in advance.
:-)
Sorry if this is overkill, but here is the majority of my implementation file:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self resetViews];
}
- (void)viewDidLoad {
[super viewDidLoad];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
[context setUndoManager:nil];
_managedObjectContext = context;
self.tableView.delegate = self;
[self setNeedsStatusBarAppearanceUpdate];
}
-(void)resetViews {
NSLog(#"\n\n\nresetViews()");
[self setupFetchedResultsController];
[self.tableView reloadData];
[self.view setNeedsDisplay];
}
- (void)setupFetchedResultsController {
NSString *entityName = #"Players";
NSLog(#"Setting up a Fetched Results Controller for the Entity named %#", entityName);
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.sortDescriptors = [NSArray arrayWithObject:
[NSSortDescriptor
sortDescriptorWithKey:#"playerName"
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
NSError *error;
NSArray *results = [_managedObjectContext executeFetchRequest:request error:&error];
_playerArray = [[NSMutableArray alloc]initWithArray:results];
NSLog(#"_playerArray count: %i", [_playerArray count]);
NSLog(#"\n");
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _playerArray.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"playerCell";
ScoringCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// Configure the cell...
Players *player_info = [_playerArray objectAtIndex:indexPath.row];
NSSet *score = player_info.scores;
for (Scoring *perObj in score){
cell.lblPlayerScore.text = [perObj.score stringValue];
NSLog(#"\n\n\n score for %#: %#", player_info.playerName, perObj.score);
}
cell.lblPlayerName.text = player_info.playerName;
cell.lblPlayerNickName.text = player_info.playerNickName;
cell.btnIncreaseScore.tag = indexPath.row;
cell.btnDecreaseScore.tag = indexPath.row;
cell.imgPlayerPhoto.image = [UIImage imageNamed:#"tmp_playerImage"];
return cell;
}
- (IBAction)increaseScore:(id)sender {
NSLog(#"PageContentViewController: increaseScore()");
UIButton* btn=(UIButton*)sender;
int selectedPlayerInt = btn.tag;
//NSLog(#"Selected row is: %d",btn.tag);
Players *player_info = [_playerArray objectAtIndex:selectedPlayerInt];
[self updateRowScore:player_info:#"add"];
}
- (IBAction)decreaseScore:(id)sender {
NSLog(#"PageContentView: decreaseScore()");
UIButton* btn=(UIButton*)sender;
int selectedPlayerInt = btn.tag;
//NSLog(#"Selected row is: %d",btn.tag);
Players *player_info = [_playerArray objectAtIndex:selectedPlayerInt];
[self updateRowScore:player_info:#"subtract"];
}
-(void)updateRowScore: (Players *)player_info :(NSString *)modifier {
NSLog(#"\n\nupdateRowScore()");
NSLog(#"Update score (%#) for: %#\n", modifier, player_info.playerName);
NSArray *scoreDataArray;
if ([self playerScoreCount:player_info] == 0) {
// NEW score... we've never scored before.
Scoring *scoring_data = [NSEntityDescription
insertNewObjectForEntityForName:#"Scoring"
inManagedObjectContext:_managedObjectContext];
//Since this is the first score, always set it to 1
scoring_data.score = [NSNumber numberWithInt:1];
scoring_data.holeNumber = [NSNumber numberWithInt:_pageIndex];
scoring_data.scoredBy = player_info;
} else {
//Update existing player score..
NSError *error = nil;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *BEntity = [NSEntityDescription entityForName:#"Scoring" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:BEntity];
NSPredicate *predicate = [NSPredicate
predicateWithFormat:#"(scoredBy = %#)", [player_info objectID]];
[fetchRequest setPredicate:predicate];
NSArray *results = [_managedObjectContext executeFetchRequest:fetchRequest error:&error];
scoreDataArray = [[NSMutableArray alloc]initWithArray:results];
Scoring *score_update = [scoreDataArray objectAtIndex:0];
int currentScore = [score_update.score intValue];
NSLog(#"current score: %d", currentScore);
if ([modifier isEqual: #"add"]) {
currentScore++;
} else {
// Don't allow negative scores.
if (currentScore >= 1) {
currentScore--;
} else {
currentScore = 0;
}
}
NSLog(#"NEW score: %d", currentScore);
score_update.score = [NSNumber numberWithInt:currentScore];
}
// write to database
[self.managedObjectContext save:nil];
[self resetViews];
}
UPDATE:
Thanks for the tip bbarnhart... I had read through that post before and had used that for a basis from which I had started. Decided to take it a step further and refactor a chunk of code using more of the Ray Wenderlich example.
I've seen some improvements to what's being recorded, and reported back through the NSLog's... but the view just still is not changing.
The action is increasing the score, and then I'm resetting the cell using [self configureCell:cell atIndexPath:path]; In there... the method that is responsible for sending text to the display... the NSLog is showing 2014-12-04 22:40:40.199 appName[7153:150248] Score for Tim: 4 when the display still only shows 3.
I know this is some stupid rookie move... I'm just doing something dead wrong that I can't figure out. Here's a snippet of the amended code.
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Players"
inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"playerName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_managedObjectContext
sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
NSError *error;
NSArray *results = [_managedObjectContext executeFetchRequest:fetchRequest error:&error];
_playerArray = [[NSMutableArray alloc]initWithArray:results];
NSLog(#"_playerArray count: %i", [_playerArray count]);
return _fetchedResultsController;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"playerCell";
ScoringCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[ScoringCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:cellIdentifier];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(ScoringCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Players *player_info = [_fetchedResultsController objectAtIndexPath:indexPath];
NSSet *scoreSet = player_info.scores;
NSString *cell_score;
for (Scoring *scoreObj in scoreSet) {
cell_score = [scoreObj.score stringValue];
}
NSLog(#"Score for %#: %#", player_info.playerName, cell_score);
if (cell_score != nil) {
cell.lblPlayerScore.text = cell_score;
}
cell.lblPlayerName.text = player_info.playerName;
cell.lblPlayerNickName.text = player_info.playerNickName;
cell.btnIncreaseScore.tag = indexPath.row;
cell.btnDecreaseScore.tag = indexPath.row;
cell.imgPlayerPhoto.image = [UIImage imageNamed:#"demo_playerb"];
[self resetViews];
NSLog(#"\n");
}
- (IBAction)increaseScore:(id)sender {
NSLog(#"PageContentViewController: increaseScore()");
UIButton *senderButton = (UIButton *)sender;
int selectedPlayerInt = senderButton.tag;
NSIndexPath *path = [NSIndexPath indexPathForRow:senderButton.tag inSection:0];
Players *player_info = [_playerArray objectAtIndex:selectedPlayerInt];
[self updateRowScore:player_info:#"add":selectedPlayerInt:path];
}
-(void)updateRowScore:(Players *)player_info :(NSString *)modifier :(int)selectedPlayerInt :(NSIndexPath *)path {
NSArray *scoreDataArray;
if ([self playerScoreCount:player_info] == 0) {
// NEW score... we've never scored before.
Scoring *scoring_data = [NSEntityDescription
insertNewObjectForEntityForName:#"Scoring"
inManagedObjectContext:_managedObjectContext];
//Since this is the first score, always set it to 1
scoring_data.score = [NSNumber numberWithInt:1];
scoring_data.holeNumber = [NSNumber numberWithInt:_pageIndex];
scoring_data.scoredBy = player_info;
} else {
//Update existing player score..
NSError *error = nil;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *BEntity = [NSEntityDescription entityForName:#"Scoring"
inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:BEntity];
NSPredicate *predicate = [NSPredicate
predicateWithFormat:#"(scoredBy = %#)", [player_info objectID]];
[fetchRequest setPredicate:predicate];
NSArray *results = [_managedObjectContext executeFetchRequest:fetchRequest error:&error];
scoreDataArray = [[NSMutableArray alloc]initWithArray:results];
Scoring *score_update = [scoreDataArray objectAtIndex:0];
int currentScore = [score_update.score intValue];
NSLog(#"current score: %d", currentScore);
if ([modifier isEqual: #"add"]) {
currentScore++;
} else {
// Don't allow negative scores.
if (currentScore >= 1) {
currentScore--;
} else {
currentScore = 0;
}
}
NSLog(#"NEW score: %d", currentScore);
score_update.score = [NSNumber numberWithInt:currentScore];
}
// write to database
[self.managedObjectContext save:nil];
static NSString *cellIdentifier = #"playerCell";
ScoringCell *cell = [_tableView dequeueReusableCellWithIdentifier:cellIdentifier];
[self configureCell:cell atIndexPath:path];
[self resetViews];
}
----------
UPDATE:
Been awhile since I've had a chance to revisit, and just noticed a new problem since enabling your tips. When scrolling down or up in the list and pulling beyond the normal boundaries, the tableview data seems to overwrite the display for the row either above or below the current line. Weird... Not sure if this animated Gif will show up in Stack. Here's an example:
The main reason your table view is not updating dynamically is NSFetchedResultsController uses a delegate for notification when changes occur. You'll need to set that delegate, self.fetchedResultsController.delegate = self and then add the delegate methods.
Here is a link to an example for managing a UITableView with a NSFetchedResultsController.
Update
Implement these NSFetchResultsController delegate methods to allow your table to be dynamically updated.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath: (NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
Generally, these methods contain boilerplate code for updating your table which you will also find in the link above.

Using a FetchedResultsController to Auto-Populate a Table view below a TextField with entries from Core Data

I am working on my first app and am in need of some assistance. I've read through tons of similar questions on SO but just not getting anywhere.
I have a simple table view controller which has a plus button; when pressed, that leads to a modal view controller asking the user to insert information into 4 separate fields. When the user clicks save, the modal view dismisses and the information is displayed in the table view because the save button calls the NSManagedObject subclasses and through Core Data, it saves it.
I'm trying to have it so that when a user types into the first field (name), if they have already typed that name before (if they added it to Core Data with the save method), it auto-populates and shows a hidden table view with entries matching that name. I first started working with a NSMutableArray but thanks to Jeff's comments, that would not persistently keep the data, so because I already have the Core Data functionality, it makes more sense to use that. I am editing this post to include how my Core Data is currently set up.
I basically want to achieve this but with Core Data (http://www.dalmob.org/2011/03/01/alternative-autocomplete-uitextfield/)
There is a Information Entity with a relationship to the People Entity.
- (IBAction)save:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
Information *information = [NSEntityDescription insertNewObjectForEntityForName:#"Information" inManagedObjectContext:context];
People *enteredPerson = (People *)[People personWithName:self.nameTextField.text inManagedObjectContext:context];
information.whichPerson = enteredPerson;
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Can't save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
The enteredPerson calls the personWithName method in the People NSManagedObjectSubclass:
+ (People *)personWithName:(NSString *)name inManagedObjectContext:(NSManagedObjectContext *)context
{
People *people = nil;
// Creating a fetch request to check whether the name of the person already exists
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"People"];
request.predicate = [NSPredicate predicateWithFormat:#"name = %#", name];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSError *error = nil;
NSArray *fetchedPeople = [context executeFetchRequest:request error:&error];
if (!fetchedPeople)
{
// Handle Error
}
else if (![fetchedPeople count])
{
// If the person count is 0 then let's create it
people = [NSEntityDescription insertNewObjectForEntityForName:#"People" inManagedObjectContext:context];
people.name = name;
}
else
{
// If the object exists, just return the last object .
people = [fetchedPeople lastObject];
}
return people;
}
Based on the suggestion to create the NSFetchRequest, I am wondering the best technique to do this.
Do I do this in the Save method of the Add Entry at the end to something like this:
// NSFetchRequest
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Specifiy a predicate here if there are certain conditions your fetch must adhere to
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY name CONTAINS[c] %#", self.nameTextField.text];
[fetchRequest setPredicate:predicate];
//NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle error
}
if ([fetchedObjects count] == 0)
{
// Add entry to results
}
What I want to achieve is, from Core Data, when the user types in the name, reference core data (with a fetch request) and if that name exists, as the user starts typing, populate the Table view that sits below the Text field.
Any guidance would be appreciated.
EDIT: I have updated an answer with some further code to almost get this working.
EDIT: More Code:
Property Declarations in .h
#property (retain, nonatomic) IBOutlet UITextField *nameTextField;
#property (nonatomic, retain) NSString *substring;
#property (weak, nonatomic) IBOutlet UITableView *testTableView;
#property (nonatomic, retain) NSFetchedResultsController* autocompleteFetchedResultsController;
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring;
ViewDidLoad
- (void)viewDidLoad
{
NSError *error;
if (![[self autocompleteFetchedResultsController] performFetch:&error])
{
NSLog(#"Unresolved error %# %#", error, [error userInfo]);
exit(-1);
}
self.testTableView.delegate = self;
self.testTableView.dataSource = self;
self.testTableView.hidden = YES;
self.testTableView.scrollEnabled = YES;
self.nameTextField.delegate = self;
[super viewDidLoad];
}
Save Method
- (IBAction)save:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
Transaction *transaction = [NSEntityDescription insertNewObjectForEntityForName:#"Transaction" inManagedObjectContext:context];
People *enteredPerson = (People *)[People personWithName:self.nameTextField.text inManagedObjectContext:context];
transaction.whoFrom = enteredPerson;
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Can't save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
Thanks,
I guess self.autocompleteUrls is the NSMutableArray u had previously... Ok, U have come a long way, now see the autocompleteFetchedResultsController -> that is what fetches, and the condition if (_autocompleteFetchedResultsController != nil) protects property method from being called every time U reference autocompleteFetchedResultsController. So U should do something like this:
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {
_autocompleteFetchedResultsController = nil;
[self autocompleteFetchedResultsController];
[self.testTableView reloadData];
}
and If U done everything else correctly that should be it...
Your cellFoRowAtIndexPath should look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"autocomplete cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewStylePlain reuseIdentifier:CellIdentifier];
}
People *people = [self.autocompleteFetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = people.name;
return cell;
}
Using the basic code from the Xcode library of snippets you can perform a Core Data fetch:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"<#Entity name#>" inManagedObjectContext:<#context#>];
[fetchRequest setEntity:entity];
// Specifiy a predicate here if there are certain conditions your fetch must adhere to
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"<#Predicate string#>", <#Predicate arguments#>];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchedObjects = [<#context#> executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle error
}
Replace the Entity name with the one that stores your NameTextField entries. And fetchedObjects is an array that will store your information you need to populate your table with.
Obviously, you will also need to save any new NameTextField entries to core data by creating a new entity and saving the context.
I have sort of got this working. Rather than update the entire question, I have left that there for reference because I am sure someone will come across a similar situation. Through the use of a FetchedResultsController object within my view controller, I'm now getting a list of names to populate the table view that sits below the text field.
Let's look at some code:
- (NSFetchedResultsController *)autocompleteFetchedResultsController
{
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
if (_autocompleteFetchedResultsController != nil)
{
return _autocompleteFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"People" inManagedObjectContext:managedObjectContext];
fetchRequest.entity = entity;
if ([self.substring length] > 0) {
NSPredicate *peoplePredicate = [NSPredicate predicateWithFormat:#"ANY name CONTAINS[c] %#", self.nameTextField.text];
[fetchRequest setPredicate:personPredicate];
}
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
self.autocompleteFetchedResultsController = theFetchedResultsController; _autocompleteFetchedResultsController.delegate = self;
return _autocompleteFetchedResultsController;
}
- (void)viewDidLoad
{
NSError *error;
// I am performing the fetchHere and if there is an error, it will get logged.
if (![[self autocompleteFetchedResultsController] performFetch:&error])
{
NSLog(#"Unresolved error %# %#", error, [error userInfo]);
exit(-1);
}
// Further code relating to tableview to make it hidden, etc
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id sectionInfo = [[_autocompleteFetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
self.testTableView.hidden = NO;
self.substring = self.nameTextField.text];
self.substring = [self.substring stringByReplacingCharactersInRange:range withString:self.substring];
[self searchAutocompleteEntriesWithSubstring:self.substring];
return YES;
}
#pragma mark UITableViewDataSource methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"autocomplete cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
People *people = [self.autocompleteFetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = people.name;
return cell;
}
#pragma mark UITableViewDelegate methods
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
self.nameTextField.text = selectedCell.textLabel.text;
}
So this works to some extent. When I place the cursor in the nameTextField, it unhides the table view, but it currently shows me the name of ALL the names already entered.
What I want is the ability to, as I'm typing, for the table to only show me what matches that.
The [self searchAutocompleteEntriesWithSubstring:substring]; in the shouldChangeCharactersInRangeMethod is calling a custom method I created.
When I had this set to a NSMutableArray instead of using Core Data, it was the code below, but I have no idea how to adjust this code to say, search core data and only display the results that match what I am already typing.
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {
self.autocompleteFetchedResultsController = nil;
[self autocompleteFetchedResultsController];
[self.testTableView reloadData];
}
I'm almost there - just need a bit of a push to get there!

Issue with deleting rows after adding section header to uitableview

Before adding section headers to one of my tables in my app, I was able to delete rows using the commitEditingStyle function without any issues. I decided to implement section headers to make it easier for the user to view items added to the table by date. This functionality works fine. I was having an issue with deleting rows after implementing the section headers but thanks to help from the good folks on stackoverflow the problem was partially resolved. After some testing I've realized that if the rows are in the same section and I try to delete more than one row in sequence beginning with the top row in the section, the top row deletes fine but trying to delete the second row causes the app to crash. If I delete all rows in sequence other than the first row and then delete the first row last, it works fine. Xcode doesn't indicate why it crashes in the debug log.
Here is the code for the cellForRowAtIndexPath function:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
AgendaCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[AgendaCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
NSString *strDate = [dateArray objectAtIndex:indexPath.section];
NSMutableArray *dateSection = [tempDict objectForKey:strDate];
NSManagedObject *object = [dateSection objectAtIndex:indexPath.row];
cell.sessionNameLabel.text = [object valueForKey:#"sessionname"];
cell.sessionNameLabel.textColor = [UIColor blueColor];
cell.sessionDateLabel.text = [object valueForKey:#"sessiondate"];
cell.sessionDateLabel.textColor = [UIColor brownColor];
cell.sessionTimeLabel.text = [object valueForKey:#"sessiontime"];
cell.sessionTimeLabel.textColor = [UIColor brownColor];
return cell;
}
Here is the code for my table refresh function:
- (void) refreshTable
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Sessnotes" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"agenda == 'Yes'"]];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"sessiondate" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
[self.refreshControl endRefreshing];
self.objects = results;
if (results.count == 0) {
NSString *message = #"You have not added any sessions to your planner.";
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:#"Notification"
message:message
delegate:self
cancelButtonTitle:#"Ok"
otherButtonTitles:nil,nil];
[alertView show];
}
else if (results.count > 0){
tempDict = nil;
tempDict = [[NSMutableDictionary alloc] init];
NSString *strPrevDate= [[results objectAtIndex:0] valueForKey:#"sessiondate"];
NSLog(#"strPrevDate value is: %#", strPrevDate);
NSString *strCurrDate = nil;
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
//Add the Similar Date data in An Array then add this array to Dictionary
//With date name as a Key. It helps to easily create section in table.
for(int i=0; i< [results count]; i++)
{
strCurrDate = [[results objectAtIndex:i] valueForKey:#"sessiondate"];
if ([strCurrDate isEqualToString:strPrevDate])
{
[tempArray addObject:[results objectAtIndex:i]];
}
else
{
[tempDict setValue:[tempArray copy] forKey:strPrevDate];
strPrevDate = strCurrDate;
[tempArray removeAllObjects];
[tempArray addObject:[results objectAtIndex:i]];
}
}
//Set the last date array in dictionary
[tempDict setValue:[tempArray copy] forKey:strPrevDate];
NSArray *tArray = [tempDict allKeys];
//Sort the array in ascending order
dateArray = [tArray sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
}
[self.tableView reloadData];
}
Here is the code for the commitEditingStyle function:
- (void)tableView:(UITableView *)tableView commitEditingStyle: (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
//add code here for when you hit delete
NSManagedObject *object = [self.objects objectAtIndex:indexPath.row];
NSManagedObjectContext *context = [self managedObjectContext];
[context deleteObject:[context objectWithID:[object objectID]]];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
NSMutableArray *array = [self.objects mutableCopy];
[array removeObjectAtIndex:indexPath.row];
self.objects = array;
[tableView reloadData];
}
}
A couple things up front. I wouldn't make fetch requests everytime you want to reload the tableview. You should look at NSFetchedResultsController. It will automatically bind data to your tableview and do the refreshes for you based on updates coming from either the same NSManagedObjectContexts or messages about updates from other contexts and batch them for you as well.
To answer your original question. I would try to remove the object from the array first and then delete the NSManagedObject and then you can use some tableview trickery:
NSManagedObject *managedObject = self.array[indexPath.row];
[self.array removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
[context deleteObject:managedObject];
I could be wrong but it's possible you're failing to fullfil a fault. Hope that helps.

Resources