nick shmick
nick shmick

Reputation: 925

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.

Upvotes: 3

Views: 1450

Answers (5)

Burhanuddin Sunelwala
Burhanuddin Sunelwala

Reputation: 5343

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];
}

Upvotes: 5

Amin Negm-Awad
Amin Negm-Awad

Reputation: 16650

If I understood your Q correctly, you have to change the schema of the model.

A. First, what I understood

You have a list of items. New items are added at the end. Now you want to give the user the ability to reorder the items in a customized manner.

B. What you do

Actually you are using a creation date attribute for ordering. Of course you cannot use this for reordering, because this would mean to change the creation date. Therefore the whole approach using a creation date fails: It is good enough for a list not changed by the user, but not, if you have a customized order.

C. What you can do

If you have a customized order, you need a customized attribute to reflect the order. If the list is a property of an instance object, you can use NSOrderedSet and Core Data's ordered relationship for doing so. I wouldn't because of pay-offs. However, you can do it, if works for you.

Otherwise you have to handle that yourself:

a. Add an attribute order to your entity type.

b. When you insert a new object, check for the count of the existing list (depends on how you hold it) and set the value as the order property of the new instance.

c. When fetching, use that property for sorting.

d. When changing, change the attribute of the instance object and of all instance objects in between source and destination. Let me explain that:

We have a lis like that:

name          order
Amin          0
Negm          1
Awad          2
Answer        3

Now, for example, Answer is moved upwards from position 3 to position 1 (ahead of Negm):

name          order
Amin          0
Answer        3
Negm          1
Awad          2

That means that the order attribute of the moved object (3) has to be changed to the new destination (1) and of all objects having an order attribute of >=1 to <3 has to be changed to +1. (And the order attribute of the moved object, too, obviously)

name          order
Amin          0
Answer        1
Negm          2
Awad          3

In Code

NSUInteger oldIndex = …; // 3
NSUInteger newIndex = …; // 1

movedObject.order = newIndex;

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Target"];
NSPredicate *betweenPredicate = [NSPredicate predicateWithFormat:@"order >= %ld AND order < %ld", newIndex, oldIndex];
NSArray *objectsToChange = [context executeFetchRequest:request error:NULL];

for( Target *target in objectsToChange )
{
  target.order = @([target.order unsignedIntegerValue] + 1);
}

If an item is moved down, you have to do the same the other way round.


If you have different lists of unique objects like playlists in iTunes, you need an extra entity type instead of an extra attribute. Let me know that, I will post the code from one of my books including moving a gapped list of items.

Upvotes: 1

Stive
Stive

Reputation: 6888

try this

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

    NSString *str1 =  [[yourArray  objectAtIndex:fromIndexPath.row] copy];
    NSString *str2 =  [[yourArray  objectAtIndex:toIndexPath.row] copy];

    [yourArray replaceObjectAtIndex:fromIndexPath.row withObject:str2];
    [yourArray replaceObjectAtIndex:toIndexPath.row withObject:str1];

    [tableView reloadData];
}

Upvotes: 1

Rajesh Loganathan
Rajesh Loganathan

Reputation: 11217

Simple Solution: Reffer Apple's Doc:

Create a NSMutableArray to identify reordered array.

Step 1: Declare a NSMutableArray property @property (strong, nonatomic) NSMutableArray *arrayTag in header or class file.

Step 2: Initialise in viewDidLoad

Step 3: Add this code in tableview delegate methods

-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    NSString *stringToMove = [arrayTag objectAtIndex:sourceIndexPath.row];
    [arrayTag removeObjectAtIndex:sourceIndexPath.row];
    [arrayTag insertObject:stringToMove atIndex:destinationIndexPath.row];
}

Upvotes: 1

Erik Johansson
Erik Johansson

Reputation: 1248

You have to change the underlying data model to reflect the new order if you want NSFetchResultsController to pick up the change.

Upvotes: 0

Related Questions