Search code examples
iosobjective-cuitableviewnsfetchedresultscontrollernsfetchrequest

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


I have a table view and I just implemented a class that helps me reorder the cells, like the regular moving cells method that comes with the table view delegate.

Now after I reorder the cells, I need to change the array that holds the cell objects to the new order... How do I do that?

This is my method for reordering the cells:

- (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { NSArray 

}

i have a coreDataStack class that takes care of all the core data stuff (creating a singelton), it looks like this:

#import "CoreDataStack.h"

@implementation CoreDataStack

#pragma mark - Core Data stack

@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;

+ (instancetype)defaultStack {

    static CoreDataStack *defaultStack;
    static dispatch_once_t onceTocken;
    dispatch_once (&onceTocken, ^{
        defaultStack = [[self alloc] init];
    });

    return defaultStack;
}


- (NSURL *)applicationDocumentsDirectory {
    // The directory the application uses to store the Core Data store file. This code uses a directory named "digitalCrown.Lister" in the application's documents directory.
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

- (NSManagedObjectModel *)managedObjectModel {
    // The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Lister" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    // The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it.
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    // Create the coordinator and store

    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Lister.sqlite"];
    NSError *error = nil;
    NSString *failureReason = @"There was an error creating or loading the application's saved data.";
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        // Report any error we got.
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data";
        dict[NSLocalizedFailureReasonErrorKey] = failureReason;
        dict[NSUnderlyingErrorKey] = error;
        error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
        // Replace this with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return _persistentStoreCoordinator;
}


- (NSManagedObjectContext *)managedObjectContext {
    // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    return _managedObjectContext;
}

#pragma mark - Core Data Saving support

- (void)saveContext {
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        NSError *error = nil;
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            // Replace this implementation with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}


@end

and whenever i add new object to core data i do it this way:

- (void)insertTeget {

    CoreDataStack *stack = [CoreDataStack defaultStack];
    Target *target = [NSEntityDescription insertNewObjectForEntityForName:@"Target" inManagedObjectContext:stack.managedObjectContext];
    if (self.myTextView.text != nil) {
        target.body = self.myTextView.text;
        target.time = [NSDate date];
    }

    [stack saveContext];

}

in the table view when I'm fetching the data i do it this way:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {


    static NSString *cellIdentifier = @"StackTableViewCell";

    Target *target = [self.fetchedResultController objectAtIndexPath:indexPath];

    StackTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (!cell)
    {
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"StackTableViewCell" owner:self options:nil];
        cell = [topLevelObjects objectAtIndex:0];
    }

    cell.cellLabel.text = target.body;

    cell.cellLabel.font = [UIFont fontWithName:@"Candara-Bold" size:20];

    cell.showsReorderControl = YES;



    // Configure the cell...

    return cell;
}

this is my fetchresultconroller/fetch request config in the table view controller class:

- (NSFetchRequest *)targetsFetchRequest {

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Target"];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"time" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];
    return fetchRequest;
}


- (NSFetchedResultsController *)fetchedResultController {

    if (_fetchedResultController != nil) {
        return _fetchedResultController;
    }

    CoreDataStack *stack = [CoreDataStack defaultStack];

    NSFetchRequest *fetchRequest = [self targetsFetchRequest];

    _fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:stack.managedObjectContext sectionNameKeyPath:nil cacheName:nil];

    _fetchedResultController.delegate = self;

    return _fetchedResultController;

}

What I want to accomplish is whenever a user create a target object it will go to the end of the array (so it will be like a queue), and if a user move cells, so I need to change the order of array of the database...

moving cells method:

- (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {

    int start = 0;
    int end = 0;
    if (fromIndexPath.row > toIndexPath.row) {
        start = (int)fromIndexPath.row;
        end = (int)toIndexPath.row;
    } else {
        start = (int)toIndexPath.row;
        end = (int)fromIndexPath.row;
    }

    for (int i = start; i <= end; ++i) {
        Target *target = [self.fetchedResultController objectAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
        [target setOrder:@(i)];
    }

    [[CoreDataStack defaultStack] saveContext];


    // a test to see if the order is changed
    [self.fetchedResultController performFetch:nil];

    NSArray *arr = [self.fetchedResultController fetchedObjects];
    for (int i=0; i<arr.count; i++)  {
        Target *ta = [arr objectAtIndex:i];
        NSLog(@"%@",ta.body);
    }
}

the log:

2015-04-14 10:29:13.405 Lister[3163:477453] One
2015-04-14 10:29:13.406 Lister[3163:477453] Two
2015-04-14 10:29:13.406 Lister[3163:477453] Three
2015-04-14 10:29:13.407 Lister[3163:477453] Four
2015-04-14 10:29:13.407 Lister[3163:477453] Five
2015-04-14 10:29:21.070 Lister[3163:477453] 

2015-04-14 10:29:21.071 Lister[3163:477453] One
2015-04-14 10:29:21.071 Lister[3163:477453] Two
2015-04-14 10:29:21.071 Lister[3163:477453] Three
2015-04-14 10:29:21.072 Lister[3163:477453] Four
2015-04-14 10:29:21.072 Lister[3163:477453] Five
2015-04-14 10:29:25.037 Lister[3163:477453] 

2015-04-14 10:29:25.039 Lister[3163:477453] One
2015-04-14 10:29:25.039 Lister[3163:477453] Two
2015-04-14 10:29:25.040 Lister[3163:477453] Three
2015-04-14 10:29:25.040 Lister[3163:477453] Four
2015-04-14 10:29:25.041 Lister[3163:477453] Five

Also , the label of the cells is acting weird now, if move the cell with the label "one" to the index of the cell with label "two", so the label of "one" is changing to "two". So i get to the situation that 2 cells have the same label.


Solution

  • Well the simplest solution would be

    1. Add an attribute to your Target entity, say it order of type Integer32.

    Creating and Inserting New Objects

    1. Whenever you create a new Target object, first fetch the existing objects from the database using sortDescriptor having key @"order" and ascending=YES. Take the last object of this fetched array and check its order. Now in your new Target object increment the order and insert it to the database. If the fetched array returns 0 objects, then set order=@(0).

      - (void)insertTeget {
      
          CoreDataStack *stack = [CoreDataStack defaultStack];
      
          //Fetching objects from database
          NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Target"];
          NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES];
          [fetchRequest setSortDescriptors:@[sortDescriptor]];
          NSArray *existingObjects = [stack.managedObjectContext executeFetchRequest:fetchRequest error:nil];
      
          //Creating new object
          Target *target = [NSEntityDescription insertNewObjectForEntityForName:@"Target" inManagedObjectContext:stack.managedObjectContext];
          if (self.myTextView.text != nil) {
              target.body = self.myTextView.text;
              target.order = @([(Target *)existingObjects.lastObject order].integerValue + 1);
          }
      
          [stack saveContext];
      }
      

    NSFetchedResultsController

    1. Fetch the objects using the above defined sortDescriptor.

    Taken from your code

        - (NSFetchRequest *)targetsFetchRequest {
    
            NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Target"];
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"order" ascending:YES];
            NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
            [fetchRequest setSortDescriptors:sortDescriptors];
            return fetchRequest;
        }
    
    
        - (NSFetchedResultsController *)fetchedResultController {
    
            if (_fetchedResultController != nil) {
                return _fetchedResultController;
            }
    
            CoreDataStack *stack = [CoreDataStack defaultStack];
            NSFetchRequest *fetchRequest = [self targetsFetchRequest];
            _fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:stack.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
            _fetchedResultController.delegate = self;
            return _fetchedResultController;
        }
    

    Rearranging cells

    1. Now while when you rearrange cells in your table view, you just need to run a for loop and update their order. You need to only update order of objects between the two indexPaths.

      - (void)moveTableView:(FMMoveTableView *)tableView moveRowFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {  
      
          int start = 0;
          int end = 0;
          if (fromIndexPath.row > toIndexPath.row) {
              start = fromIndexPath.row;
              end = toIndexPath.row;
          } else {
              start = toIndexPath.row;
              end = fromIndexPath.row;
          }
      
          for (int i = start; i <= end; ++i) {
              Target *target = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
              [target setOrder:@(i)];
          }
      
          [[CoreDataStack defaultStack] saveContext];
      }
      

    Note: The above solution assumes that you have order beginning from 0.


    When you create and insert new Target objects you need to implement NSFetchedResultsController delegate methods to add corresponding rows for those objects. Since we have already defined sortDescriptor, the new rows will be added at the end of the tableView.

    - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
        [self.tableView beginUpdates];
    }
    
    
    - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
        atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    
        switch(type) {
            case NSFetchedResultsChangeInsert:
                [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                                withRowAnimation:UITableViewRowAnimationFade];
                break;
        }
    }
    
    
    - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
        atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
        newIndexPath:(NSIndexPath *)newIndexPath {
    
        UITableView *tableView = self.tableView;
    
        switch(type) {
    
            case NSFetchedResultsChangeInsert:
                [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                           withRowAnimation:UITableViewRowAnimationFade];
                break;
    
            case NSFetchedResultsChangeUpdate:
                break;
    
            case NSFetchedResultsChangeMove:
                [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                           withRowAnimation:UITableViewRowAnimationFade];
                [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                           withRowAnimation:UITableViewRowAnimationFade];
                break;
        }
    }
    
    
    - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
        [self.tableView endUpdates];
    }