Core Data Fetch To Many - iphone

I am trying to fetch all categories, and their sub categories, and display them all in a table. I know how to fetch all categories, but I need to fetch all sub categories, and sort them by category using a fetch results controller. Any ideas of suggestions?

You can create a fetched results controller that fetches SubCategory entities and groups them into sections according to the Category:
// Fetch "SubCategory" entities:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SubCategory"];
// First sort descriptor for grouping the cells into sections, sorted by category name:
NSSortDescriptor *sort1 = [NSSortDescriptor sortDescriptorWithKey:#"category.name" ascending:YES];
// Second sort descriptor for sorting the cells within each section:
NSSortDescriptor *sort2 = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObjects:sort1, sort2, nil];
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:#"category.name"
cacheName:nil];
[self.fetchedResultsController setDelegate:self];
NSError *error;
BOOL success = [self.fetchedResultsController performFetch:&error];
Then you can use the usual table view data source methods as described in the NSFetchedResultsController Class Reference.
This gives you a table view with one table view section for each category.

so, you have the categories in the fetchedResultsController.fetchedObjects
since each subCategory is essentially contained in the Category you can access each by calling [Category valueForKey:#"subCategory"
this will give you an NSSet that you can then sort out (to an NSArray) and use as data for your tableView.
It won't be contained in a fetchedResultsController though.

If you have the option u can do it in other way also if u like.
Take all The Category objects in arrayOfCategories
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection (NSInteger)section:
{
Category *cat = [ arrayOfCategories objectAtIndex:indexPath.row]
if(arrayToHoldObjects.count > 0)
{
[arrayToHoldObjects removeAllObject];
}
for(Subcategory *sub in Category.subcategory)
{
[arrayToHoldObjects addObject:sub];
}
return arrayToHoldObjects.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Category *cat = [ arrayOfCategories objectAtIndex:indexPath.row]
if(arrayToHoldObjects.count > 0)
{
[arrayToHoldObjects removeAllObject];
}
for(Subcategory *sub in Category.subcategory)
{
[arrayToHoldObjects addObject:sub];
}
Subcategory *sub = [arrayToHoldObjects objectAtIndexPath.row]
for(int k =0 ; k < arrayToHoldObjects .count; k++)
{
// do what ever u like with sub
return cell;
}
}

Related

Order the tableview cell by the subtitle

i'm getting the distance of 2 points like this
[userLocation distanceFromLocation: annotationLocation] / 1000;
and setting this to the subtitle of a tableview like the image bellow
the question is, can i order this table by the distances (subtitle)?
Thanks!
and sorry for the bad english =x
You can order your UITableView's cells any way to want to, but you have to do it before showing them, when you create the table's data source. If you use an NSFetchResultsController, you can put the distance as the sort descriptor. And if you are using a simple NS(Mutable)Array, sort it before making it the table's source.
Like Chris said, you can do it with
NSSortDescriptor *titleSorter= [[NSSortDescriptor alloc] initWithKey:#"annotationLocation" ascending:YES];
If it is an NSArray what you are using, then:
[arrayOfObjects sortUsingDescriptors:[NSArray arrayWithObject:titleSorter];
and if it is an NSFetchResultsController:
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:titleSorter, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
Create an NSSortDescriptor to sort your rows:
NSSortDescriptor *titleSorter= [[NSSortDescriptor alloc] initWithKey:#"annotationLocation" ascending:YES];
[arrayOfObjects sortUsingDescriptors:[NSArray arrayWithObject:titleSorter];
Make an array with these distances, order it how you want and
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"cellIdentifier";
UITableViewCell *_cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (_cell == nil) {
_cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier] autorelease];
}
_cell.textLabel.text = #"Transcripts";
_cell.detailTextLabel.text = [yourArray objectAtIndex:indexPath.row];
return _cell;
}
well, something like that should do the trick.
Hope it helps
Assuming you have some certain objects holding a coordinate and put the objects in to an array locations, you can use a comparator block by doing:
locations = [locations sortedArrayUsingComparator: ^(id a, id b) {
CLLocationDistance dist_a= [[a objectsForKey:#"coordinate"] distanceFromLocation: userPosition];
CLLocationDistance dist_b= [[b objectsForKey:#"coordinate"] distanceFromLocation: userPosition];
if ( dist_a < dist_b ) {
return (NSComparisonResult)NSOrderedAscending;
} else if ( dist_a > dist_b) {
return (NSComparisonResult)NSOrderedDescending;
} else {
return (NSComparisonResult)NSOrderedSame;
}
}
You add this code to your viewWillAppear: to get updated locations each time you display the tableView.

Can't Tell If Fetch Or Relationship Is The Problem

I am adding object exercise to object session (as a relationship).
In a view, I want to fetch and display exercises for a particular session object.
Right now it is showing all exercises in the database rather than just for that session object.
The relationship between the two objects is called "exercises".
This is the current code I am using for the fetch if anyone can help me.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Exercise" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
LogResultsViewController *logResultsTableViewController = [[LogResultsViewController alloc]init];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat: #"exercises = %#", logResultsTableViewController.selectedSession]];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
Updated Code:
- (void) viewDidLoad
{
NSSet *exercises = [self.selectedSession valueForKey:#"exercises"];
NSArray *sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"timeStamp" ascending:YES]];
NSArray *sorted = [exercises sortedArrayUsingDescriptors:sortDescriptors];
sorted = self.exerciseArray;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [exerciseArray count];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = [exerciseArray objectAtIndex:indexPath.row];
return cell;
}
When I NSLog the selectedSession, here is what it shows:
selectedSession: <Session: 0x7336940> (entity: Session; id: 0x7334f70 <x-coredata://17D44726-23F7-402F-9CBE-2EED96212E14/Session/p1> ; data: {
exercises = "<relationship fault: 0x5d34680 'exercises'>";
timeStamp = "2011-05-31 04:41:07 +0000";
Also, when I NSLog the NSSset called exercises, I get:
Relationship fault for (<NSRelationshipDescription: 0x711b370>), name exercises, isOptional 1, isTransient 0, entity Session, renamingIdentifier exercises, validation predicates (
), warnings (
), versionHashModifier (null), destination entity Exercise, inverseRelationship exercises, minCount 0, maxCount 0 on 0x7140790
Update:
Ok so I changed the code in cellForRowAtIndex to have
Exercise *exercise = (Exercise *)[exerciseArray objectAtIndex:indexPath.row];
cell.textLabel.text = exercise.name;
Now it shows the exercise name but it is showing the same list of exercises for all sessions, instead of just for the session to which it belongs to.
If your instances of Session have a relationship to instances of Exercise, you don't need to do another fetch to get the session's exercises. You can just follow the relationship and get them directly-- they'll be loaded automatically. From your code it looks like logResultsTableViewController.selectedSession is an instance of Session. If that's the case, you can get all of that session's exercies as follows (assuming the relationship is called exercises):
NSSet *exercises = [logResultsTableViewController.selectedSession valueForKey:#"exercises"];
You can then sort that NSSet as needed.

Indexing results from an NSFetchedResultsController

I'm having some issues with a mixed, indexed, searchable results set from an NSFetchedResults controller. I have it set up to store an indexed A-Z first initial for an entity, and then want it to display numeric first initials (i.e. # as the UILocalizedIndexCollation would do).
I have already written the code that saves a "firstInitial" attribute of an Artist object as NSString #"#" if the full name started with a number, and I seem to have gotten the code half working in my UITableViewController with a customised sort descriptor. The problem is that it only works until I quit/relaunch the app.
At this point, the # section from the fetched results appears at the top. It will stay there until I force a data change (add/remove a managed object) and then search for an entry, and clear the search (using a searchDisplayController). At this point the section re-ordering will kick in and the # section will be moved to the bottom...
I'm obviously missing something/have been staring at the same code for too long. Alternatively, there's a much easier way of doing it which I'm not aware of/can't find on Google!
Any help would be appreciated!
Thanks
Sean
The relevant code from my UITableViewController is below.
- (void)viewDidLoad
{
// ----------------------------------
// Various other view set up things in here....
// ...
// ...
// ----------------------------------
NSError *error;
if (![[self artistResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Failed to fetch artists: %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
- (NSFetchedResultsController *)artistResultsController {
if (_artistResultsController != nil) {
return _artistResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Artist" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *initialSort = [[NSSortDescriptor alloc]
initWithKey:#"firstInitial"
ascending:YES
comparator:^(id obj1, id obj2) {
// Various number conditions for comparison - if it's a # initial, then it's a number
if (![obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedAscending;
else if ([obj1 isEqualToString:#"#"] && ![obj2 isEqualToString:#"#"]) return NSOrderedDescending;
if ([obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedSame;
// Else it's a string - compare it by localized region
return [obj1 localizedCaseInsensitiveCompare:obj2];
}];
NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:initialSort, nameSort, nil]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_context
sectionNameKeyPath:#"firstInitial"
cacheName:nil];
self.artistResultsController = theFetchedResultsController;
_artistResultsController.delegate = self;
[nameSort release];
[initialSort release];
[fetchRequest release];
[_artistResultsController release];
return _artistResultsController;}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[[_artistResultsController sections] objectAtIndex:section] name];
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[NSArray arrayWithObject:UITableViewIndexSearch] arrayByAddingObjectsFromArray:
[[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]];
}
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return 0;
} else {
if (title == UITableViewIndexSearch) {
[tableView scrollRectToVisible:self.searchDisplayController.searchBar.frame animated:NO];
return -1;
}
else {
for (int i = [[_artistResultsController sections] count] -1; i >=0; i--) {
NSComparisonResult cr =
[title localizedCaseInsensitiveCompare:
[[[_artistResultsController sections] objectAtIndex:i] indexTitle]];
if (cr == NSOrderedSame || cr == NSOrderedDescending) {
return i;
}
}
return 0;
}
}
}
EDIT: Forgot to mention - my search filter is using a predicate on the fetchedResults controller, so this causes a new fetch request, like so
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
NSFetchRequest *aRequest = [_artistResultsController fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
// set predicate to the request
[aRequest setPredicate:predicate];
// save changes
NSError *error = nil;
if (![_artistResultsController performFetch:&error]) {
NSLog(#"Failed to filter artists: %#, %#", error, [error userInfo]);
abort();
}
}
I ended up going about fixing this a different way.
SortDescriptors seem to have issues with running a custom sort when you are also using CoreData with SQLite for your backend storage. I tried a few things; NSString categories with a new comparison method, the compare block as listed above, and refreshing the table multiple times to try and force an update with the sort criterion.
In the end, I couldn't force the sort descriptor to do an initial sort, so I changed the implementation. I set the firstInitial attribute for artists whose names began with numerics to 'zzzz'. This means that CoreData will sort this correctly (numerics last) off the bat.
After doing this, I then hardcoded my titleForHeaderInSection method to return # for the title if appropriate, as below:
if ([[[[_artistResultsController sections] objectAtIndex:section] indexTitle] isEqualToString:#"zzzz"]) return [NSString stringWithString:#"#"];
return [[[_artistResultsController sections] objectAtIndex:section] indexTitle];
Essentially this means it's sorting numbers into a 'zzzz' grouping, which should be last, and I'm just ignoring that title and saying the title is # instead.
Not sure if there's a better way to do this, but it keeps all of the sorting inside CoreData, which is probably more efficient/scalable in the long run.

Core Data Error "Fetch Request must have an entity"

I've attempted to add the TopSongs parser and Core Data files into my application, and it now builds succesfully, with no errors or warning messages. However, as soon as the app loads, it crashes, giving the following reason:
UPDATE: I've got it all working, but my TableView doesn't show any data, and the app doesn't respond to the following breakpoints.
Thanks.
UPDATE: Here's the new code that doesn't respond to the breakpoints.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)table {
return [[fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (void)viewDidUnload {
[super viewDidUnload];
self.tableView = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.managedObjectContext];
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellIdentifier = #"SongCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
}
Incident *incident = [fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:NSLocalizedString(#"#%d %#", #"#%d %#"), incident.title];
return cell;
}
- (void)tableView:(UITableView *)table didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[table deselectRowAtIndexPath:indexPath animated:YES];
self.detailController.incident = [fetchedResultsController objectAtIndexPath:indexPath];
[self.navigationController pushViewController:self.detailController animated:YES];
}
UPDATE: Here's the code where all instances of fetch are found.
- (Category *)categoryWithName:(NSString *)name {
NSTimeInterval before = [NSDate timeIntervalSinceReferenceDate];
#ifdef USE_CACHING
// check cache
CacheNode *cacheNode = [cache objectForKey:name];
if (cacheNode != nil) {
// cache hit, update access counter
cacheNode.accessCounter = accessCounter++;
Category *category = (Category *)[managedObjectContext objectWithID:cacheNode.objectID];
totalCacheHitCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheHitCount++;
return category;
}
#endif
// cache missed, fetch from store - if not found in store there is no category object for the name and we must create one
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:self.categoryEntityDescription];
NSPredicate *predicate = [self.categoryNamePredicateTemplate predicateWithSubstitutionVariables:[NSDictionary dictionaryWithObject:name forKey:kCategoryNameSubstitutionVariable]];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchResults = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
NSAssert1(fetchResults != nil, #"Unhandled error executing fetch request in import thread: %#", [error localizedDescription]);
Category *category = nil;
if ([fetchResults count] > 0) {
// get category from fetch
category = [fetchResults objectAtIndex:0];
} else if ([fetchResults count] == 0) {
// category not in store, must create a new category object
category = [[Category alloc] initWithEntity:self.categoryEntityDescription insertIntoManagedObjectContext:managedObjectContext];
category.name = name;
[category autorelease];
}
#ifdef USE_CACHING
// add to cache
// first check to see if cache is full
if ([cache count] >= cacheSize) {
// evict least recently used (LRU) item from cache
NSUInteger oldestAccessCount = UINT_MAX;
NSString *key = nil, *keyOfOldestCacheNode = nil;
for (key in cache) {
CacheNode *tmpNode = [cache objectForKey:key];
if (tmpNode.accessCounter < oldestAccessCount) {
oldestAccessCount = tmpNode.accessCounter;
[keyOfOldestCacheNode release];
keyOfOldestCacheNode = [key retain];
}
}
// retain the cache node for reuse
cacheNode = [[cache objectForKey:keyOfOldestCacheNode] retain];
// remove from the cache
[cache removeObjectForKey:keyOfOldestCacheNode];
} else {
// create a new cache node
cacheNode = [[CacheNode alloc] init];
}
cacheNode.objectID = [category objectID];
cacheNode.accessCounter = accessCounter++;
[cache setObject:cacheNode forKey:name];
[cacheNode release];
#endif
totalCacheMissCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheMissCount++;
return category;
}
And this one...
- (void)fetch {
NSError *error = nil;
BOOL success = [self.fetchedResultsController performFetch:&error];
NSAssert2(success, #"Unhandled error performing fetch at SongsViewController.m, line %d: %#", __LINE__, [error localizedDescription]);
[self.tableView reloadData];
}
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Song" inManagedObjectContext:managedObjectContext]];
NSArray *sortDescriptors = nil;
NSString *sectionNameKeyPath = nil;
if ([fetchSectioningControl selectedSegmentIndex] == 1) {
sortDescriptors = [NSArray arrayWithObjects:[[[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES] autorelease], [[[NSSortDescriptor alloc] initWithKey:#"rank" ascending:YES] autorelease], nil];
sectionNameKeyPath = #"category.name";
} else {
sortDescriptors = [NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:#"rank" ascending:YES] autorelease]];
}
[fetchRequest setSortDescriptors:sortDescriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:sectionNameKeyPath cacheName:#"SongsCache"];
}
return fetchedResultsController;
}
your extra caching is probably a waste of cycles as Core Data performs its own caching internally. I am willing to bet you are slowing things down rather than speeding them up, not to mention the additional memory you are consuming.
Where are you setting categoryEntityDescription? That is now shown in the code you posted. It is probably nil.
Why are you retaining an NSEntityDescription?!? They are already in memory because of Core Data and retaining them is a waste which could lead to issues if Core Data wants to release it at some point.
update
Your caching is definitely not coming from Apple's code because they know that the cache is in Core Data.
As for the NSEntityDescription, again, do not retain the NSEntityDescription.
Are you 100% positive that the NSEntityDescription is not nil? Have you confirmed it in the debugger? Have you tested it with a freshly retrieved NSEntityDescription?
update
You need to learn to use the debugger as that will solve most of your coding issues. Put a breakpoint in this method and run your code in the debugger. Then when the execution stops on that break point you can inspect the values of the variables and learn what they are currently set to. That will confirm or deny your suspicions about what is and is not nil.
This error you are seeing happens when you fail to set the Entity in the NSFetchRequest which, based on your code, means that retained property is not being set before the code you have shown is being called.
Based on the code posted and the problem description, I suspect that the categoryEntityDescription property is returning nil.
I've seen this happen when the NSEntityDescription given to a fetch request is nil. The most likely cause of that is that you have a model entity that is named differently from the name you provided to entityForName. Barring that, it could be an error in configuration of your Core Data stack or a missing data model, but as a first step, I would recommend storing the result of entityForName in a local variable and breaking there to make sure it isn't nil.
Since you added the model file manually, is the .xcdatamodel file inside the Compile Sources step in your Target?
Go to the Targets entry in the Groups & Files pane in Xcode and click the disclosure triangle. Then click on the disclosure triangle for your app. Then check to see if it's in Compile Sources. If not, right click on Compile Sources and choose "Add -> Existing File..." and add it.
Edit based on update:
UPDATE: Here's the new code that
doesn't respond to the breakpoints.
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)table didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Is your view controller set as the UITableViewDataSource/UITableViewDelegate for your UITableView? If not, these methods will not get called.

How can I maintain display order in UITableView using Core Data?

I'm having some trouble getting my Core Data entities to play nice and order when using an UITableView.
I've been through a number of tutorials and other questions here on StackOverflow, but there doesn't seem to be a clear or elegant way to do this - I'm really hoping I'm missing something.
I have a single Core Data entity that has an int16 attribute on it called "displayOrder". I use an NSFetchRequest that has been sorted on "displayOrder" to return the data for my UITableView. Everything but reordering is being respected. Here is my (inefficient) moveRowAtIndePath method:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
[self FF_fetchResults];
for (NSUInteger i = 0; i < [self.fetchedResultsController.fetchedObjects count]; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, i);
otherObject.displayOrderValue = i;
}
[self FF_fetchResults];
}
Can anyone point me in the direction of a good bit of sample code, or see what I'm doing wrong? The tableview display updates OK, and I can see through my log messages that the displayOrder property is being updated. It's just not consistently saving and reloading, and something feels very "off" about this implementation (aside from the wasteful iteration of all of my FFObjects).
Thanks in advance for any advice you can lend.
I took a look at your code and this might work better:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
NSUInteger start, end;
int delta;
if (fromIndex < toIndex) {
// move was down, need to shift up
delta = -1;
start = fromIndex + 1;
end = toIndex;
} else { // fromIndex > toIndex
// move was up, need to shift down
delta = 1;
start = toIndex;
end = fromIndex - 1;
}
for (NSUInteger i = start; i <= end; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);
otherObject.displayOrderValue += delta;
}
[self FF_fetchResults];
}
(This is intended as as comment on gerry3's answer above, but I am not yet able to comment on other users' questions and answers.)
A small improvement for gerry3's - very elegant - solution. If I'm not mistaken, the line
otherObject.displayOrderValue += delta;
will actually perform pointer arithmetic if displayOrderValue is not of primitive type. Which may not be what you want. Instead, to set the value of the entity, I propose:
otherObject.displayOrderValue = [NSNumber numberWithInt:[otherObject.displayOrderValue intValue] + delta];
This should update your entity property correctly and avoid any EXC_BAD_ACCESS crashes.
Here a full solution how to manage an indexed table with core data. Your attribute is called displayOrder, I call it index.
First of all, you better separate view controller and model. For this I use a model controller, which is the interface between the view and the model.
There are 3 cases you need to manage that the user can influence via the view controller.
Adding a new object
Deleting an existing object
Reorder objects.
The first two cases Adding and Deleting are pretty straightforward. Delete calls a routine called renewObjectIndicesUpwardsFromIndex in order to update the indices after the deleted object.
- (void)createObjectWithTitle:(NSString*)title {
FFObject* object = [FFObject insertIntoContext:self.managedObjectContext];
object.title = title;
object.index = [NSNumber numberWithInteger:[self numberTotalObjects]];
[self saveContext];
}
- (void)deleteObject:(FFObject*)anObject {
NSInteger objectIndex = [anObject.index integerValue];
[anObject deleteObject];
[self renewObjectIndicesUpwardsFromIndex:objectIndex];
[self saveContext];
}
- (void)renewObjectIndicesUpwardsFromIndex:(NSInteger)fromIndex {
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index > %d)", fromIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
NSInteger index = fromIndex;
for (FFObject* object in objects) {
object.index = [NSNumber numberWithInteger:index];
index += 1;
}
[self saveContext];
}
Before I come to the controller routines for the re-order, here the part in the view controller. I use a bool isModifyingOrder similar to this answer. Notice that the view controller calls two functions in the controller moveObjectOrderUp and moveObjectOrderDown. Depending on how you display the objects in the table view - newest first or newest last - you can switch them.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
isModifyingOrder = YES;
NSUInteger fromIndex = sourceIndexPath.row;
NSUInteger toIndex = destinationIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
NSInteger delta;
if (fromIndex < toIndex) {
delta = toIndex - fromIndex;
NSLog(#"Moved down by %lu cells", delta);
[self.objectController moveObjectOrderUp:affectedObject by:delta];
} else {
delta = fromIndex - toIndex;
NSLog(#"Moved up by %lu cells", delta);
[self.objectController moveObjectOrderDown:affectedObject by:delta];
}
isModifyingOrder = NO;
}
And here the part in the controller. This can be written nicer, but for understanding this is maybe best.
- (void)moveObjectOrderUp:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue] - delta;
NSInteger toIndex = [affectedObject.index integerValue];
if (fromIndex < 1) {
return;
}
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index >= %d) AND (index < %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects) {
NSInteger newIndex = [object.index integerValue] + 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:fromIndex];
[self saveContext];
}
- (void)moveObjectOrderDown:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue];
NSInteger toIndex = [affectedObject.index integerValue] + delta;
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index > %d) AND (index <= %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects)
{
NSInteger newIndex = [object.index integerValue] - 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:toIndex];
[self saveContext];
}
Don't forget to use a second BOOL in your view controller for the delete action to prevent the move notification to do anything. I call it isDeleting and put it here.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
if (isModifyingOrder) return;
...
switch(type) {
...
case NSFetchedResultsChangeMove:
if (isDeleting == false) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:localIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:localNewIndexPath]withRowAnimation:UITableViewRowAnimationFade];
}
break;
...
}
}
I think that:
affectedObject.displayOrderValue = toIndex;
must be placed after:
for (NSUInteger i = start; i <= end; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);
otherObject.displayOrderValue += delta;
}
and before:
[self FF_fetchResults];
The answers above (as far as I can tell) only work if cell is being moved up or down in the same section. For this approach to be valid, one would have to prevent the user from moving between sections. (Using the canMoveRowAt indexPath: IndexPath -> Bool tableView delegate method).
To maintain display order in a UITableView when moving a cell within a section, or to a different section, here is code I stole verbatim from https://github.com/MrAlek/Swift-NSFetchedResultsController-Trickery/blob/ceac7937a3b20f78d7268274b18eef4845917090/CoreDataTrickerySwift/ToDoViewController.swift
This code uses a ToDo as the NSManagedObject subclass.
Here is the meat of the logic:
func updateInternalOrderForToDo(_ toDo: ToDo, sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {
// Update internal order to reflect new position
// First get all toDos, in sorted order
var sortedToDos = fetchedResultsController.fetchedObjects!
sortedToDos = sortedToDos.filter() {$0 != toDo} // Remove current toDo
// Insert toDo at new place in array
var sortedIndex = destinationIndexPath.row
for sectionIndex in 0..<destinationIndexPath.section {
sortedIndex += toDoListController.sections[sectionIndex].numberOfObjects
if sectionIndex == sourceIndexPath.section {
sortedIndex -= 1 // Remember, controller still thinks this toDo is in the old section
}
}
sortedToDos.insert(toDo, at: sortedIndex)
// Regenerate internal order for all toDos
for (index, toDo) in sortedToDos.enumerated() {
toDo.metaData.internalOrder = sortedToDos.count-index
}
}
}
There is some tweaking of the moveRowAt depending on whether one is directly updating a cell, moving to a different section, and or using Snapshots with DiffableData source. A key concept is to temporarily disable NSFetchedResultsController delegate calls to update the table. This is covered elsewhere, including a link referenced above. The code below is not intended to by copy / pasted, but just to illustrate some of the considerations of what should be handled in moveRowAt call
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
if sourceIndexPath == destinationIndexPath {
return
}
// Don't let fetched results controller affect table view
fetchControllerDelegate.ignoreNextUpdates = true
// Trust that we will get a toDo back
let toDo = toDoListController.toDoAtIndexPath(sourceIndexPath)!
//manually update managedObject properties as necessary to reflect inclusion in the new section.
if sourceIndexPath.section != destinationIndexPath.section {
//handle changes to managed objects propertie(s) here
}
// Table view is in inconsistent state, update the cell. If you are using a DiffableDataSource skip this and see below
if let cell = tableView.cellForRow(at: destinationIndexPath) {
self.configureCell(cell, toDo: toDo)
}
//see method implementation above
updateInternalOrderForToDo(toDo, sourceIndexPath: sourceIndexPath, destinationIndexPath: destinationIndexPath)
// Save
try! toDo.managedObjectContext!.save()
//if you are using DiffableDataSource, call apply(newSnapshot, animatingDifferences: animated) on your UIDifffableDataSource object after saving the moc. make sure ignoreNextUpdates bool is still set to true before calling save()
}