UITableView doesn't reload on model change with NSFetchedResultsController - uitableview

Got a problem logically deleting an object from table view. Idea is to set a flag to an item like "deleted" to 1 if item shouldn't be shown. Loading data predicate shouldn't load such rows.
Code with predicate and creation of NSFetchedResultController:
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Photo"];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"section" ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES]];
request.predicate = [NSPredicate predicateWithFormat:#"tag = %# and deleted == %#", self.tag, #(0)];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.tag.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
Deletion happens on swipe. Here is action for the swipe:
- (IBAction)deletePhoto:(UISwipeGestureRecognizer *)sender {
CGPoint location = [sender locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (indexPath) {
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.tag.managedObjectContext performBlock:^{
photo.deleted = #(1); //This must trigger data change and view must be redrawn. But it's not happending for some reason.
}];
}
}
The problem here is that row becomes deleted only after application restart. For some reason FetchedResultsController doesn't remove changed value from data and table view still shows the row.
I tried remove the item from table view explicitly but got exception for incorrect number of item in section 0 (that's why I assume that the row is still in fetch result). Explicit call to [self.tableView reloadData] or [self performFetch] don't reload the data and item still there.
I'm almost ran out of ideas what should I do to reload the data.
Thanks in advance.

I shouldn't name a field in DB "deleted". Renamed it and now app works fine.

Related

How to fetch Core Data by the order the data was added and display in that order in a UITableView?

I am displaying an entity called Skills in a UITableViewController.
I fetch the results like this in the viewDidLoad:
-(void)fetchTableData {
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Skills" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error;
self.skillsArray = [[self.managedObjectContext executeFetchRequest:fetchRequest error:&error] mutableCopy];
}
Also my cell for index path is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
// Configure the cell...
Skills *skill = self.skillsArray[indexPath.row];
// Skills is a NSManagedObject, I added the Skills.h file.
[cell.textLabel setText:skill.nameOfSkill];
return cell;
}
And I am adding new NSManagedObject *newSkill to Core Data by using UIAlertView with a text field in the delegate method:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) {
NSLog(#"Add button clicked");
NSString *newSkillText = [alertView textFieldAtIndex:0].text;
NSManagedObjectContext *context = [self managedObjectContext];
Skills *newSkill = [NSEntityDescription insertNewObjectForEntityForName:#"Skills" inManagedObjectContext:context];
newSkill.nameOfSkill = newSkillText;
[self.skillsArray addObject:newSkill];
} else {
// Do something else
}
[self.tableView reloadData];
}
Every time I reload the data the cells are displaying the data in the order the data was added but if dismiss the view controller and return the cells display the data in a different order than added? The weird part is that I am using this same exact code to add core data and retrieve it in another UITableViewController and it never displays out of order. The data added in this UITableViewController is as follows: I am pushing to another UIViewController and add the information there and then dismiss back to the tableview. In this code I am adding the information while in the view controller is being presented, maybe that could have something to do with it?
Also I know I could add an NSSortDiscriptor such as:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]initWithKey:#"nameOfSkill" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
but it does it by the name and I want by the way it was added without having to add another attribute for index cause I never did that to my other data and it always displays in the order it was added.
You'll have to add an Attribute to sort on...either an updating, incrementing counter, or the timestamp of the insertion. If you subclass NSManagedObject, ou can write this value in -awakeFromInsert. Then your tableview's fetch request will sort on that attribute.
You won't get the data as it is. You will have to insert a field of "Time" and you can sort according to it.
OR
Add a unique field of 'data_id' . Always check the count before inserting the data. And give the data_id accordingly adding 1 to the count. Then after fetching the data from core data sort it as per data_id.
You can do as per you like.
For the desired result you need to sort it according to timestamp or primary key which is auto-generated by core data database.
Core Data makes its own primary key - you don't have to add one. You can retrieve it with
NSManagedObjectID *moID = [managedObject objectID];

Get indexPath of Core Data object in UITableView

I have a UITableView that I'm populating from Core Data with the following NSfetchedResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
I have another method attached to an "Add" button that adds a new Core Data item to database. As soon as the object is added, my table updates properly and the new object is shown in its correct spot according to the "date" sort in the fetched results controller. My new object is added using today's date for its "date" attribute. I add the object like this:
NSManagedObject *newEvent = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
[newEvent setValue:#"New Client" forKey:#"name"];
[newEvent setValue:[NSDate date] forKey:#"date"];
NSError *error;
if (![context save:&error]) {
NSLog(#"Core Data error! Could not save: %#", [error localizedDescription]);
}
Now, as part of my "Add" method, I need to select the row where the new item was added and segue to an edit screen. Obviously, depending on the other dates of items in the table, it could be anywhere.
I want to select it like this, but I don't have the indexPath:
[self.eventListTable selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionBottom];
How do I determine which row (indexPath) my new object was added at so that I can select it properly?
Thanks!
NSFetchedResultsController have a method called: indexPathForObject:
If you have inserted items during your change processing (the FRC delegate methods), select the most recent inserted item. you can determine the index path of the object using the method above.
Or, you could keep the inserted object from your last insert, and in the didChangeContent delegate method, select the inserted item and nullify the variable you kept (so further calles won't trigger the segue).
You will need to find the entity in your NSFetchedResultsController to resolve its indexPath. However to do that, you need to wait for your NSFetchedResultsController to recognize the object. You will probably need to wait for the delegate callback methods from the NSFetchedResultsController to fire and then use -indexPathForObject: to resolve it back to an indexPath and then select the row.
Visually it should work perfectly. The row will appear and then get selected.

CoreData query returns zero rests on subObjects initially - but after quitting all the data exists

I'm using CoreData and I have an entity called a FlightRecording contained inside of this entity is a set of messages AhrsMesages. I'm having strange behavior where I make a recording and then once I save the recording I load a table of past recordings which should show the number of messages per-recording. What is happening, however, is that it shows 0 messages. If I quit the application and restart-it and go into my recording list menu again it will correctly show the correct number of sub-entites.
I use the following code in ViewDidLoad of my PreviousFlightVC.m to load my flights:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"FlightRecording"];
NSSortDescriptor *originSort =
[[NSSortDescriptor alloc] initWithKey:#"originAirport" ascending:YES];
NSSortDescriptor *destinationSort =
[[NSSortDescriptor alloc] initWithKey:#"destinationAirport" ascending:YES];
NSSortDescriptor *dateSort =
[[NSSortDescriptor alloc] initWithKey:#"recordingStart" ascending:YES];
fetchRequest.sortDescriptors = #[originSort, destinationSort, dateSort];
[fetchRequest setIncludesSubentities:YES];
self.frc = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
self.frc.delegate = self;
NSError *fetchingError = nil;
if ([self.frc performFetch:&fetchingError]) {
NSLog(#"Success Fetch");
} else {
NSLog(#"Fetch Error");
}
For each cell I'm generating the Count as such:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecordingRowCell *cell = nil;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
/* - IPAD Version */
cell = [tableView dequeueReusableCellWithIdentifier:#"RecordingRowCell"];
} else {
/* Iphone table Cells */
cell = [tableView dequeueReusableCellWithIdentifier:#"iphoneFlightCell"];
}
FlightRecording *rec = [[[self frc] fetchedObjects] objectAtIndex:[indexPath row]];
// Count the records
int recordCount = [rec.ahrsMessages count];
NSLog(#"%d", recordCount);
// ...
Does anybody have any ideas why I might be seeing this behavior? Could I some how have left the "data" open for writing? Any suggestions would be great. Its a minor annoyance but I still can't exactly figure out why it is happening.
Here is my initial table (before quitting) - notice msgCount = 0
After i quit the app and restart here is my table - notice msgCount = 6
NSFetchedResultsController does not track changes in related objects to the ones it is fetching (in relationships).
In your case, an addition to the ahrsMessages set will not be reflected in the currently fetched objects.
In any case, your count fetching in cellForRow... is very expensive performance wise.
It will be better to store the count in the FlightRecording object itself.
each time you ask for the count for an object it need to resolve the to-many relationship (a trip to the store).
Each time you add a message to a recording, increase the count of recordings in the related recording object (which will be faulted-in anyway by CoreData).
This will also cause your FRC to hear of the change and update the view.

NSFetchedResultsController delegate method calls on predicate change

I have a simple app, where I have countries which have cities, which in turn have people. I want to display list of countries in a table view. I use NSFetchedResultsController to get all the data. This is the setup :
-(void)initializeFetchedResultsController
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Country"];
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
fetchRequest.fetchBatchSize = 30;
self.fetchResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext managedObjectContext]
sectionNameKeyPath:#"nameFirstLetter"
cacheName:nil];
self.fetchResultsController.delegate = self;
[self.fetchResultsController performFetch:nil];
}
I also added an ability to search by typing in the country name in the search bar, so I implemented UISearchBarDelegate method :
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if([searchText isEqualToString:#""])
{
self.fetchResultsController.fetchRequest.predicate = nil;
}
else
{
self.fetchResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
}
[self.fetchResultsController performFetch:nil];
[self.tableView reloadData];
}
This works but not the way I want. I expected that on predicate change, my NSFetchResultsControllerDelegate delegate methods will be called again so I can insert or delete items / sections from my table view (i want animations) without having to figure out myself, what has been added and removed. But this is not the case, instead if I change the predicate, delegate methods are not called, and I must do a simple [self.tableView reloadData]. Am I doing something wrong or is it just the way it is supposed to work, and I cannot take this shortcut ?
You are not doing it wrong. This is the way it is and there's no shortcut to take. You'll have to implement your own code to animate the table view between fetches.
As #StianHøiland says, you need to do it yourself. Generally with delegate methods they are called as a result of an 'offline' / asynchronous change in order to notify you. They are not called as a result of a change you have explicitly requested.
You could think about using the fetchedObjects and the indexPathForObject: features of the FRC. Filter the fetchedObjects list (using your predicate). Get the index paths for the objects that have been removed and you can animate them out.

Delete Object from Core data relationship - NSSet

I have a one to many relationship between a Product entity and a Cart Entity.
The user needs to have the opportunity to delete a product from the cart.
I fetch the products like so:
// Fetch request for "Product":
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Product"];
// Fetch only products for the cart:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"inCart = %#", self.cart];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"navn" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_theManagedObjectContext sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
and populate the tableview:
Product *prod = (Product *)[self.fetchedResultsController objectAtIndexPath:indexPath];
In the tableview I have a button and when you tap it, that current product, should get deleted... I have the following code in my "delete product method"
-(void)RemoveFromCart:(UIButton *)sender {
UITableViewCell *cell = (UITableViewCell *)sender.superview;
NSIndexPath *indexPath = [_Table indexPathForCell:cell];
Product *prod = (Product *)[self.fetchedResultsController objectAtIndexPath:indexPath];
/*_cart is retrieved when the user pushes the add to cart button, in another viewcontroller, a cart object is then returned and passed to the cart viewcontroller */
NSMutableSet *mutableSet = [NSMutableSet setWithSet:_cart.products];
[mutableSet removeObject:prod];
_cart.products = mutableSet;
[self saveCurrentContext:_theManagedObjectContext];
[_Table reloadData];
}
But nothing happens, when the method is fired.. the product is not removed from the relationship.
How do I manage to fix this, my code is obviously wrong somewhere.
To remove a product from the cart, you can either use
product.inCart = nil;
or
[_cart removeProductsObject:product];
(Both lines are equivalent if the inverse relationships are properly set up.)
[_theManagedObjectContext deleteObject:product];
This only worked for me after Xcode 7.
Maybe because of the changes introduced about the entity+CoreDataProperties category creation?

Resources