How to Implement Re-Ordering of Coredata Records

How to implement re-ordering of CoreData records?

FetchedResultsController and its delegate are not meant to be used for user-driven model changes. See the Apple reference doc.
Look for User-Driven Updates part. So if you look for some magical, one-line way, there's not such, sadly.

What you need to do is make updates in this method:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
userDrivenDataModelChange = YES;

...[UPDATE THE MODEL then SAVE CONTEXT]...

userDrivenDataModelChange = NO;
}

and also prevent the notifications to do anything, as changes are already done by the user:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (userDrivenDataModelChange) return;
...
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if (userDrivenDataModelChange) return;
...
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (userDrivenDataModelChange) return;
...
}

I have just implemented this in my to-do app (Quickie) and it works fine.

Persist and rearrange the order of Core Data records

Core Data expects you to define your own sorting with NSSortDescriptors, so you'll have to add your own attribute to track a user-customizable ordering index like that. I've had luck using KVO on the entity's relationship keypath (from within the managed object itself) to change the ordering as needed when new relationship objects are added or removed.

To handle rearrangement, you'll want to take advantage of NSTableView's drag and drop methods and reset the order index attribute as needed. The NSTableView documentation should explain it pretty well.

How to change records order in coredata?

CoreData does not have any ordering.

What you have to do is to order the data you fetch. For example if you use a NSFetchRequest you can add an array of NSSortDescriptors which specify the sort ordering of the fetched data

So, for example:

NSFetchRequest *request = ....
request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];

EDIT: after the comment:
If the ordering must be persisted to the CoreData store, the only way I see is to add in your model an integer attribute. By default you can leave it at -1, i.e., no order.
When you perform the drag and drop in the UI you can then save the index to the attribute and persist it in the store.

Your fetch request can become something like

 request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES], [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];

In this way in case of equal ordering (-1) the name will be taken into account, otherwise the index have priority.

SwiftUI reorder CoreData Objects in List

Caveat: the answer below is untested, although I used parallel logic in a sample project and that project seems to be working.

There's a couple parts to the answer. As Joakim Danielson says, in order to persist the user's preferred order you will need to save the order in your CoreItem class. The revised class would look like:

public class CoreItem: NSManagedObject, Identifiable{
@NSManaged public var name: String
@NSManaged public var userOrder: Int16
}

The second part is to keep the items sorted based on the userOrder attribute. On initialization the userOrder would typically default to zero so it might be useful to also sort by name within userOrder. Assuming you want to do this, then in CoreItemList code:

@FetchRequest( entity: CoreItem.entity(),
sortDescriptors:
[
NSSortDescriptor(
keyPath: \CoreItem.userOrder,
ascending: true),
NSSortDescriptor(
keyPath:\CoreItem.name,
ascending: true )
]
) var coreItems: FetchedResults

The third part is that you need to tell swiftui to permit the user to revise the order of the list. As you show in your example, this is done with the onMove modifier. In that modifier you perform the actions needed to re-order the list in the user's preferred sequence. For example, you could call a convenience function called move so the modifier would read:

.onMove( perform: move )

Your move function will be passed an IndexSet and an Int. The index set contains all the items in the FetchRequestResult that are to be moved (typically that is just one item). The Int indicates the position to which they should be moved. The logic would be:

private func move( from source: IndexSet, to destination: Int) 
{
// Make an array of items from fetched results
var revisedItems: [ CoreItem ] = coreItems.map{ $0 }

// change the order of the items in the array
revisedItems.move(fromOffsets: source, toOffset: destination )

// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].userOrder =
Int16( reverseIndex )
}
}

Technical reminder: the items stored in revisedItems are classes (i.e., by reference), so updating these items will necessarily update the items in the fetched results. The @FetchedResults wrapper will cause your user interface to reflect the new order.

Admittedly, I'm new to SwiftUI. There is likely to be a more elegant solution!

Paul Hudson (Hacking With Swift) has quite a bit more detail. Here is a link for info on moving data in a list. Here is a link for using core data with SwiftUI (it involves deleting items in a list, but is closely analogous to the onMove logic)

How to persist reordering items when using a single entity in CoreData?

If these items are related to some other item, you could use an ordered to-many relationship from that other item. That's the only option provided by Core Data.

For what it's worth, if you add a sort index, you don't need to update "every" other record when the user changes the order. You only need to update those between the start and end position of the change. For example if the user moves something from index 2 to index 12, the item at index 1 is still at index 1, and the items at index 13 through 80 billion also have the same index.

UITableView Core Data reordering

I'm not sure which part you are having trouble with (based on the comments)... but here is my suggestion. The displayOrder is just a simple attribute on a NSManagedObject class. If you can save a managed object, then you will be able finish this feature. Lets first take a simple NSManagedObject:

@interface RowObj :  NSManagedObject  
{
}

@property (nonatomic, retain) NSString *rowDescription;
@property (nonatomic, retain) NSNumber *displayOrder;

Next, we need to have local copy of the data being displayed in the tableview. I have read through the comments you have made and I'm not really sure if you are using the FetchedResultsController or not. My suggestion would be to start simple and just use a normal tableviewcontroller where you update the row data whenever a user changes the display order... then save the order when the user is done editing.

The interface for this tableviewcontoller would look like this:

@interface MyTableViewController : UITableViewController {
NSMutableArray *myTableViewData;
}

@property(nonatomic,retain) NSMutableArray *myTableViewData;

@end

Next, we need to load the the table view data in the viewWillAppear method:

- (void)viewWillAppear:(BOOL)animated {
myTableViewData = [helper getRowObjects]; // insert method call here to get the data
self.navigationItem.leftBarButtonItem = [self editButtonItem];
}

There are 2 things going on here... (I'll explain the editButtonItem later) the first is that we need to get our data from CoreData. When I have to do this I have some sort of helper(call it what you want) object do the work. A typical find method would look like this:

- (NSMutableArray*) getRowObjects{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"RowObj" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];

NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
[request release];
return mutableFetchResults;
}

Now that you have your data, you can now wait for the user to edit the table. That is where the [self editButtonItem] comes into play. This is a built in feature that returns a bar button item that toggles its title and associated state between Edit and Done. When the user hits that button, it will invoke the setEditing:animated: method:

To update the display order you need to override the setEditing method on the UITableViewController class. It should look something like this:

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[myTableView setEditing:editing animated:animated];
if(!editing) {
int i = 0;
for(RowObj *row in myTableViewData) {
row.displayOrder = [NSNumber numberWithInt:i++];
}
[helper saveManagedObjectContext]; // basically calls [managedObjectContext save:&error];
}
}

We don't have to do anything when the user is in edit mode... we only want to save once they have pressed the "Done" button. When a user drags a row in your table you can update your display order by overriding the canMoveRowAtIndexPath and moveRowAtIndexPath methods:

- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return true;
}

(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {

RowObj *row = [myTableViewData objectAtIndex:sourceIndexPath.row];
[myTableViewData removeObjectAtIndex:sourceIndexPath.row];
[myTableViewData insertObject:row atIndex:destinationIndexPath.row];
}

Again, the reason I don't update the displayOrder value here is because the user is still in edit mode... we don't know if the user is done editing AND they could even cancel what they've done by not hitting the "Done" button.

EDIT

If you want to delete a row you need to override tableView:commitEditingStyle:forRowAtIndexPath and do something like this:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object at the given index path.
RowObj *row = [myTableViewData objectAtIndex:indexPath.row];
[helper deleteRow:row];

// Update the array and table view.
[myTableViewData removeObjectAtIndex:indexPath.row];
[myTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}

Reordering UITableView with NSOrderedSet in CoreData

Hi i implemented it like this. The "currentObject" is root object of the relationship and "subItems" the name of the ordered relationship in the model which is managed by the UITableViewController.

 - (void)moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{

NSMutableOrderedSet* orderedSet = [self.currentObject mutableOrderedSetValueForKey:@"subItems"];

NSInteger fromIndex = fromIndexPath.row;
NSInteger toIndex = toIndexPath.row;

// see http://www.wannabegeek.com/?p=74
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:fromIndex];

if (fromIndex > toIndex) {
// we're moving up
[orderedSet moveObjectsAtIndexes:indexes toIndex:toIndex];
} else {
// we're moving down
[orderedSet moveObjectsAtIndexes:indexes toIndex:toIndex-[indexes count]];
}

[self.dataStore saveObjectContext];
}


Related Topics



Leave a reply



Submit