如何实现CoreData记录的重新排序?

时间:2009-07-03 02:38:03

标签: ios core-data

我正在为我的iPhone应用程序使用CoreData,但CoreData不提供允许您重新排序记录的自动方式。我想使用另一列来存储订单信息,但使用连续的数字来排序索引有一个问题。如果我处理大量数据,重新排序记录可能涉及更新订购信息上的大量记录(这有点像改变数组元素的顺序)

实施有效订购方案的最佳方法是什么?

10 个答案:

答案 0 :(得分:89)

FetchedResultsController及其委托不应用于用户驱动的模型更改。见the Apple reference doc。 查找用户驱动的更新部分。所以,如果你寻找一些神奇的,单行的方式,那就不可能了。

您需要做的是使用此方法进行更新:

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

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

 userDrivenDataModelChange = NO;
}

并且还阻止通知执行任何操作,因为用户已完成更改:

- (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;
 ...
}

我刚刚在我的待办事项应用程序(Quickie)中实现了它,它运行正常。

答案 1 :(得分:14)

这是一个快速示例,显示了将获取的结果转储到NSMutableArray中的方法,该NSMutableArray用于移动单元格。然后,您只需更新名为orderInTable的实体上的属性,然后保存托管对象上下文。

这样,您不必担心手动更改索引,而是让NSMutableArray为您处理。

创建一个可用于暂时绕过NSFetchedResultsControllerDelegate

的BOOL
@interface PlaylistViewController ()
{
    BOOL changingPlaylistOrder;
}
@end

表视图委托方法:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    // Refer to https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14

    // Bypass the delegates temporarily
    changingPlaylistOrder = YES;

    // Get a handle to the playlist we're moving
    NSMutableArray *sortedPlaylists = [NSMutableArray arrayWithArray:[self.fetchedResultsController fetchedObjects]];

    // Get a handle to the call we're moving
    Playlist *playlistWeAreMoving = [sortedPlaylists objectAtIndex:sourceIndexPath.row];

    // Remove the call from it's current position
    [sortedPlaylists removeObjectAtIndex:sourceIndexPath.row];

    // Insert it at it's new position
    [sortedPlaylists insertObject:playlistWeAreMoving atIndex:destinationIndexPath.row];

    // Update the order of them all according to their index in the mutable array
    [sortedPlaylists enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Playlist *zePlaylist = (Playlist *)obj;
        zePlaylist.orderInTable = [NSNumber numberWithInt:idx];
    }];

    // Save the managed object context
    [commonContext save];

    // Allow the delegates to work now
    changingPlaylistOrder = NO;
}

你的代表现在看起来像这样:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    if (changingPlaylistOrder) return;

    switch(type)
    {
        case NSFetchedResultsChangeMove:
            [self configureCell:(PlaylistCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (changingPlaylistOrder) return;

    [self.tableView reloadData];
}

答案 2 :(得分:8)

迟到的回复:也许您可以将排序键存储为字符串。在两个现有行之间插入记录可以通过向字符串添加附加字符来简单地完成,例如,在行“A”和“B”之间插入“AM”。不需要重新排序。类似的想法可以通过在4字节整数上使用浮点数或一些简单的位运算来实现:插入一个行,其排序键值是相邻行之间的一半。

如果字符串太长,浮点数太小,或者int中没有空间,可能会出现病态情况,但是您可以重新编号实体并重新开始。在极少数情况下扫描和更新所有记录比每次用户重新排序时对每个对象进行错误处理要好得多。

例如,考虑int32。使用高3字节作为初始排序可以为您提供近1700万行,并且能够在任意两行之间插入多达256行。 2个字节允许在重新扫描之前在任意两行之间插入65000行。

这是我想到的伪代码,用于2字节增量和2字节用于插入:

AppendRow:item
    item.sortKey = tail.sortKey + 0x10000

InsertRow:item betweenRow:a andNextRow:b
    item.sortKey = a.sortKey + (b.sortKey - a.sortKey) >> 1

通常你会调用AppendRow导致sortKeys为0x10000,0x20000,0x30000等的行。有时你需要InsertRow,比如说在第一个和第二个之间,导致sortKey为0x180000。

答案 3 :(得分:5)

我已经使用双值来实现@andrew / @dk的方法。

您可以在github上找到UIOrderedTableView

随意分叉:)

答案 4 :(得分:5)

我从Matt Gallagher的博客中找到了这个方法(找不到原始链接)。如果您有数百万条记录,这可能不是最佳解决方案,但会推迟保存,直到用户完成对记录的重新排序。

- (void)moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath sortProperty:(NSString*)sortProperty
{
    NSMutableArray *allFRCObjects = [[self.frc fetchedObjects] mutableCopy];
    // Grab the item we're moving.
    NSManagedObject *sourceObject = [self.frc objectAtIndexPath:sourceIndexPath];

    // Remove the object we're moving from the array.
    [allFRCObjects removeObject:sourceObject];
    // Now re-insert it at the destination.
    [allFRCObjects insertObject:sourceObject atIndex:[destinationIndexPath row]];

    // All of the objects are now in their correct order. Update each
    // object's displayOrder field by iterating through the array.
    int i = 0;
    for (NSManagedObject *mo in allFRCObjects)
    {
        [mo setValue:[NSNumber numberWithInt:i++] forKey:sortProperty];
    }
    //DO NOT SAVE THE MANAGED OBJECT CONTEXT YET


}

- (void)setEditing:(BOOL)editing
{
    [super setEditing:editing];
    if(!editing)
        [self.managedObjectContext save:nil];
}

答案 5 :(得分:2)

实际上,有一种更简单的方法,使用“双”类型作为排序列。

然后,只要您重新订购,您只需要重置重新订购商品的订单属性值:

reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;

答案 6 :(得分:1)

我最终放弃了编辑模式下的FetchController,因为我还需要重新排序我的表格单元格。我想看一个有效的例子。相反,我继续使用mutablearray作为表的当前视图,并保持CoreData orderItem atrribute一致。

NSUInteger fromRow = [fromIndexPath row]; 
NSUInteger toRow = [toIndexPath row]; 



 if (fromRow != toRow) {

    // array up to date
    id object = [[eventsArray objectAtIndex:fromRow] retain]; 
    [eventsArray removeObjectAtIndex:fromRow]; 
    [eventsArray insertObject:object atIndex:toRow]; 
    [object release]; 

    NSFetchRequest *fetchRequestFrom = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityFrom = [NSEntityDescription entityForName:@"Lister" inManagedObjectContext:managedObjectContext];

    [fetchRequestFrom setEntity:entityFrom];

    NSPredicate *predicate; 
    if (fromRow < toRow) predicate = [NSPredicate predicateWithFormat:@"itemOrder >= %d AND itemOrder <= %d", fromRow, toRow];  
    else predicate = [NSPredicate predicateWithFormat:@"itemOrder <= %d AND itemOrder >= %d", fromRow, toRow];                          
    [fetchRequestFrom setPredicate:predicate];

    NSError *error;
    NSArray *fetchedObjectsFrom = [managedObjectContext executeFetchRequest:fetchRequestFrom error:&error];
    [fetchRequestFrom release]; 

    if (fetchedObjectsFrom != nil) { 
        for ( Lister* lister in fetchedObjectsFrom ) {

            if ([[lister itemOrder] integerValue] == fromRow) { // the item that moved
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:toRow];               
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            } else { 
                NSInteger orderNewInt;
                if (fromRow < toRow) { 
                    orderNewInt = [[lister itemOrder] integerValue] -1; 
                } else { 
                    orderNewInt = [[lister itemOrder] integerValue] +1; 
                }
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:orderNewInt];
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            }

        }

        NSError *error;
        if (![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();  // Fail
        }           

    }                                   

}   

如果有人使用fetchController有解决方案,请发布它。

答案 7 :(得分:1)

所以花了一些时间来解决这个问题......!

上面的答案是很好的构建基础,没有它们我会迷失方向,但与其他受访者一样,我发现他们只是部分工作。如果你实现它们,你会发现它们工作一次或两次,然后出错,或者你丢失了数据。下面的答案远非完美 - 这是很多深夜,反复试验的结果。

这些方法存在一些问题:

  1. 链接到NSMutableArray的NSFetchedResultsController并不保证更新上下文,因此您可能会发现这有时会起作用,但不会发生其他情况。

  2. 用于交换对象的复制然后删除方法也是难以预测的行为。我在其他地方发现了在引用已在上下文中删除的对象时出现不可预测行为的引用。

  3. 如果你使用对象索引行并有部分,那么这不会表现得很好。上面的一些代码只使用.row属性,不幸的是,这可能会引用yt中的多行

  4. 使用NSFetchedResults Delegate = nil,对于简单的应用程序是可以的,但考虑到您要使用委托捕获将复制到数据库的更改,然后您可以看到这不能正常工作。

  5. 核心数据并不像正确的SQL数据库那样真正支持排序和排序。上面的for循环解决方案很好,但是应该有一种正确的数据排序方式 - IOS8? - 所以你需要进入这个期望你的数据将遍布整个地方。

  6. 人们针对这些帖子发布的问题涉及很多这些问题。

    我有一个简单的桌面应用程序,其中包含部分&#39;部分&#39;工作 - 我仍然处理不明原因的用户界面行为,但我相信我已经深入了解......

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

    这是通常的代表

    {
    userDrivenDataModelChange = YES;
    

    使用if()返回结构的上述信号量机制。

    NSInteger sourceRow = sourceIndexPath.row;
    NSInteger sourceSection = sourceIndexPath.section;
    NSInteger destinationRow = destinationIndexPath.row;
    NSInteger destinationSection = destinationIndexPath.section;
    

    并非所有这些都在代码中使用,但将它们用于调试很有用

    NSError *error = nil;
    NSIndexPath *destinationDummy;
    int i = 0;
    

    变量的最终初始化

    destinationDummy = [NSIndexPath indexPathForRow:0 inSection:destinationSection] ;
    // there should always be a row zero in every section - although it's not shown
    

    我在隐藏的每个部分中使用第0行,这会存储部分名称。即使在其中没有“实时记录”,这也允许该部分可见。我使用第0行来获取部分名称。这里的代码有点凌乱,但是它完成了这项工作。

    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];    
    NSManagedObject *currentObject = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
    NSManagedObject *targetObject = [self.fetchedResultsController objectAtIndexPath:destinationDummy];
    

    获取上下文以及源和目标对象

    然后,此代码创建一个新对象,该对象从源获取数据,从目标中获取该部分。

    // set up a new object to be a copy of the old one
    NSManagedObject *newObject = [NSEntityDescription
                                  insertNewObjectForEntityForName:@"List"
                                inManagedObjectContext:context];
    NSString *destinationSectionText = [[targetObject valueForKey:@"section"] description];
    [newObject setValue:destinationSectionText forKeyPath:@"section"];
    [newObject setValue: [NSNumber numberWithInt:9999999] forKey:@"rowIndex"];
    NSString *currentItem = [[currentObject valueForKey:@"item"] description];
    [newObject setValue:currentItem forKeyPath:@"item"];
    NSNumber *currentQuantity =[currentObject valueForKey:@"quantity"] ;
    [newObject setValue: currentQuantity forKey:@"rowIndex"];
    

    现在创建一个新对象并保存上下文 - 这会欺骗移动操作 - 您可能无法在新删除的位置获取新记录 - 但至少它将位于正确的部分。

    // create a copy of the object for the new location
    [context insertObject:newObject];
    [context deleteObject:currentObject];
    if (![context 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();
    }
    

    现在执行如上所述的for循环更新。请注意,在我执行此操作之前已保存上下文 - 不知道为什么需要这样做,但它不能正常工作!

    i = 0;
    for (NSManagedObject *mo in [self.fetchedResultsController fetchedObjects] )
    {
        [mo setValue:[NSNumber numberWithInt:i++] forKey:@"rowIndex"];
    }
    if (![context 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();
    }
    

    重新设置信号量并更新表格

    userDrivenDataModelChange = NO;
    
    [tableView reloadData];
    

    }

答案 8 :(得分:1)

这就是我正在做的事情似乎有效。对于每个实体,我都有一个createDate,用于在创建表时对表进行排序。它也是一个独特的钥匙。所以我在移动时所做的就是交换源和目的地日期。

我希望在执行saveContext之后正确地对表进行排序,但是会发生两个单元格彼此叠加的情况。所以我重新加载数据并更正订单。从头开始应用程序显示记录仍按正确顺序排列。

不确定这是一般解决方案甚至是正确的,但到目前为止它似乎都有效。

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    HomeEntity* source_home = [self getHomeEntityAtIndexPath:sourceIndexPath];
    HomeEntity* destination_home = [self getHomeEntityAtIndexPath:destinationIndexPath];
    NSTimeInterval temp = destination_home.createDate;
    destination_home.createDate = source_home.createDate;
    source_home.createDate = temp;

    CoreDataStack * stack = [CoreDataStack defaultStack];
    [stack saveContext];
    [self.tableView reloadData];
}

答案 9 :(得分:-1)

尝试查看iPhone here的核心数据教程。其中一节讨论排序(使用NSSortDescriptor)。

您可能还会发现Core Data basics页面非常有用。