Deleting Core Data from a UITableView within a UIViewController - ios

I have a UITableView within a UIViewController so that I can have a UILabel below the table. In doing so, I have had difficulties in adding an Edit/Done button. I couldn't do the traditional way, so I had to do a work around using the following idea:
1)Create the edit button up the top left on viewdidload:
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:#selector(editbutton)];
2)Create code so that upon clicking the edit button, the tableview becomes editable, and it changes the title to done. Then upon clicking done, it goes back to saying edit.
-(IBAction)editbutton{
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(donebutton)];
[tableView setEditing:YES animated:YES];
}
-(IBAction)donebutton{
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:#selector(editbutton)];
[tableView setEditing:NO animated:YES];
}
This part is all OK. I just have put it in for completeness. The tableview becomes editable when click the edit button, and i can click done and it goes back to normal. My problem is clicking the delete button (after clicking the Red minus button next to a row) doesn't delete the row. I have tried the following code:
NOTE:
1)context has been declared in the .h file as:
#property (nonatomic, retain) NSManagedObjectContext *context;
and synthesized in the .m file.
2)I have declared the #property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController in the .h file and then the #synthesize fetchedResultsController = _fetchedResultsController in the .m file
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object for the given index path
[context deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error = nil;
if (![context 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();
}
}
}
EDIT:
Ok, I have found a bit of a solution. I used the following code to delete my core data:
- (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]];
// Save the context.
NSError *error = nil;
if (![context 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();
}
[self fetchedresults];
[self.tableView reloadData];
}
}
My new problem is that when i click delete. The row is deleted but still remains there with the red minus button on a blank row. I can still click on the row too (which normally edits the data) but there is no data to load.
EDIT 2:
I forgot to add, to get it to work, i added this:
- (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:#"Record" inManagedObjectContext:context];
[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:#"activity" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects: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:context sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
EDIT 3:
This is what fetched results does:
- (void)fetchedresults {
NSManagedObjectContext *moc = [self context];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Record" inManagedObjectContext:moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#"activity" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil)
{
// Deal with error...
}
float tot = [[array valueForKeyPath:#"#sum.cpdhours"] floatValue];
totalhours.text = [NSString stringWithFormat:#"%.1f", tot];
}

I think that the managedObjectContext property you have is actually a parent context to your fetchedResultsController context, and thus when you delete an entity using it, the fetchedResultsController won't know that it's supposed to refetch and update the tableview. Try calling [self.fetchedResultsController.managedObjectContext deleteItem:yourItem].
Maybe it's not exactly like that I'm writing this on my iPhone but you get the idea. Also have you made sure to implement the fetched results controller delegate methods to update your tableview?

Your probably will need to recache your NSResultFetchedController. In your NSFetchedResultController init function, your are caching your fetch result into a cache named "Master". This probably explain the behavior you're experiencing.
You can either not used any cache by setting the cache name to nil when setting up the NSFetchedResultController
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
or you delete the cache right after you deleted your NSManagedObject.
[NSFetchedResultController deleteCacheWithName:#"Master"];

Are you using an NSFetchedResultsController? If not, try it and you shouldn't worry about reloading the table at all.
Also, there's absolutely no need to use a custom button. Just stick with the .editButtonItemand override -setEditing:animated:.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
[self.tableView setEditing:editing animated:animated];
}

Swift: In my case I want to completely wipe my tableView after the user signs out:
func clearData(){
NSFetchedResultsController.deleteCacheWithName("MyCache")
let indices = tableView.allIndices
let moc = NSManagedObjectContext.MR_defaultContext()
let fetchRequest = NSFetchRequest(entityName: "MyEntity")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
saveMoc()
do {
try moc.persistentStoreCoordinator!.executeRequest(deleteRequest, withContext: moc)
try fetchedResultsController.performFetch()
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(indices, withRowAnimation: .Automatic)
tableView.endUpdates()
} catch let e as NSError {
print(e)
}
}
I made an extension to get all the NSIndexPaths for my tableview:
extension UITableView{
var allIndices: [NSIndexPath] {
var indices = [NSIndexPath]()
let sections = self.numberOfSections
if sections > 0{
for s in 0...sections - 1 {
let rows = self.numberOfRowsInSection(s)
if rows > 0{
for r in 0...rows - 1{
let index = NSIndexPath(forRow: r, inSection: s)
indices.append(index)
}
}
}
}
return indices
}
}

Related

Attempting to reordering UITableView using an NSFetchedResultsController

I am attempting to edit the order of my UITableView while using Core Data and an NSFetchedResultsController. As I understand, Core Data does not have a built in method for rearranging objects in a Core Data model.
The idea was to create an array, reorder my objects there, and then write that data back to my model.
NSFetchedResultsController
-(NSFetchedResultsController *) fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"List" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"listName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc]initWithObjects:sortDescriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
moveRowAtIndexPath
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; {
NSMutableArray *toDoItems = [[self.fetchedResultsController fetchedObjects] mutableCopy];
NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];
[toDoItems removeObject:managedObject];
[toDoItems insertObject:managedObject atIndex:[destinationIndexPath row]];
int i = 0;
for (NSManagedObject *moc in toDoItems) {
[moc setValue:[NSNumber numberWithInt:i++] forKey:#"listName"];
}
[self.managedObjectContext save:nil];
}
When I hit my Edit button, I can rearrange the rows but as soon as I let the row go, my app crashes. Im not getting any kind of stack trace in the console when it crashes.
I set a breakpoint on exception and it seems to be crashing on this line
[moc setValue:[NSNumber numberWithInt:i++] forKey:#"listName"];
My key name is correct. But I now realize this is completely wrong in that I am trying to set this as a number and that shouldnt be the case.
Any suggestion or push in the right direction would be appreciated.
Either amend your code to set a string value for the listName, something like this:
[moc setValue:[NSString stringWithFormat:"%d",i++] forKey:#"listName"];
(but beware because by default this will sort as a string, so 11 comes before 2, etc).
So, better, add an integer attribute to your model, and use that to sort the fetched results controller.
Don't copy NSManagedObject. It's a managed object by core data. If you want a new one ask the context.

Saving the edited UITableView Rows

So basically I have a table view of folder objects and I want to be able to remove/ delete folders. So far if I try to delete, the folders are removed, but when I re run the application they are all back (so the delete is not saved). Any advice?
here is my delete method for the UITableView:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[self.folders removeObjectAtIndex:indexPath.row];
NSMutableArray *newSavedFolders = [[NSMutableArray alloc] init];
for (Folder *folder in self.folders){
[newSavedFolders addObject:[self folderWithName:folder.name]];
}
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
and the folderWithName method is from here:
- (Folder *)folderWithName:(NSString *)name {
id delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [delegate managedObjectContext];
Folder *folder = [NSEntityDescription insertNewObjectForEntityForName:#"Folder" inManagedObjectContext:context];
folder.name = name;
folder.date = [NSDate date];
NSError *error;
if (![context save:&error]) {
//we have an error
}
return folder;
}
The reason it deletes is because of this line:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
That removes the row from view, but doesn't remove the data.
If your using CoreData then you simply do the following with a NSFetchedResultsController:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
[self.tableView reloadData];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
Add call your items with the NSFetchedResultsController this way:
#pragma mark - Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Folder" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number for displaying in UITableView.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
}

moveRows with NSFetchedResultsController bug

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.

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!

Filtering a UITableView from NSFetchedResultsController and Core Data with NSPredicate

I have a UITableView using NSFetchedResultsController to display a list of users.
I added a UISegmentedControl to switch between my full list of users and only my active users.
To get my list of users, I use fetchedResultsController :
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"User" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastname" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
if (self.barSegmentedControl.selectedSegmentIndex == 1) {
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"active == YES"]];
}
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
To perform an update of the tableview when clicking on the UISegmentedControl, I use segmentedControlIndexChanged :
-(IBAction) segmentedControlIndexChanged{
self.fetchedResultsController = nil;
[self fetchedResultsController];
[self.tableView reloadData];
}
But I'm not sure I'm doing this right.
Could you say me if this is the right way to filter a UITableView with NSFetchedResultsController ?
I also wanted to know if it is possible to filter a list with an animation ?
Exactly like in the iPhone Phone App when in the Recents Tab, it switch from all calls to missed calls ?
Thanks for your help.
Your approach will work fine. I too using the same thing without any issues.
Coming to animation you can do them in its delegate methods,
Check Apple's Documentation here.
self.fetchedResultsController = nil; is a reasonable way to invalidate a property.
But you should delete this weird [self fetchedResultsController]; line.
[self.tableView reloadData]; will call your delegate which should access fetchedResultsController property (if you're using _fetchedResultsController variable in your datasource methods, that would be an anti-pattern).
filter a list with an animation
Prepare a list of rows to hide and send
[self.tableView deleteRowsAtIndexPaths: ... withRowAnimation:...]
instead of reloadData.

Resources