ios 如何实现 CoreData 记录的重新排序?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1077568/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to implement re-ordering of CoreData records?
提问by Boon
I am using CoreData for my iPhone app, but CoreData doesn't provide an automatic way of allowing you to reorder the records. I thought of using another column to store the order info, but using contiguous numbers for ordering index has a problem. if I am dealing with lots of data, reordering a record potentially involves updating a lot of records on the ordering info (it's sorta like changing the order of an array element)
我在 iPhone 应用程序中使用 CoreData,但 CoreData 不提供允许您重新排序记录的自动方式。我想使用另一列来存储订单信息,但是使用连续数字进行排序索引有问题。如果我处理大量数据,重新排序记录可能涉及更新排序信息上的大量记录(有点像更改数组元素的顺序)
What's the best way to implement an efficient ordering scheme?
实施有效订购方案的最佳方法是什么?
回答by Aleksandar Vaci?
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.
FetchedResultsController 及其委托不打算用于用户驱动的模型更改。请参阅Apple 参考文档。寻找用户驱动的更新部分。因此,如果您寻找某种神奇的单行方式,可悲的是没有这样的方式。
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.
我刚刚在我的待办事项应用程序 (Quickie) 中实现了它,它运行良好。
回答by iwasrobbed
Here is a quick example showing a way to dump the fetched results into an NSMutableArray which you use to move the cells around. Then you just update an attribute on the entity called orderInTable
and then save the managed object context.
这是一个快速示例,展示了一种将获取的结果转储到 NSMutableArray 中的方法,您可以使用它来移动单元格。然后您只需更新被调用实体的属性orderInTable
,然后保存托管对象上下文。
This way, you don't have to worry about manually changing indexes and instead you let the NSMutableArray handle that for you.
这样,您不必担心手动更改索引,而是让 NSMutableArray 为您处理。
Create a BOOL that you can use to temporarily bypass the NSFetchedResultsControllerDelegate
创建一个 BOOL,您可以使用它来暂时绕过 NSFetchedResultsControllerDelegate
@interface PlaylistViewController ()
{
BOOL changingPlaylistOrder;
}
@end
Table view delegate method:
表视图委托方法:
- (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;
}
Your delegates would look something like this now:
你的代表现在看起来像这样:
- (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];
}
回答by dk.
A late reply: perhaps you could store the sort key as a string. Inserting a record between two existing rows can be done trivially by adding an additional character to a string, e.g. inserting "AM" between the rows "A" and "B". No reordering is required. A similar idea could be accomplished by using a floating point number or some simple bit arithmetic on a 4-byte integer: insert a row with a sort key value that is half way between the adjacent rows.
迟到的回复:也许您可以将排序键存储为字符串。在两个现有行之间插入记录可以通过向字符串添加附加字符来轻松完成,例如在行“A”和“B”之间插入“AM”。无需重新排序。类似的想法可以通过对 4 字节整数使用浮点数或一些简单的位算术来实现:插入一行,其排序键值位于相邻行的中间。
Pathological cases could arise where the string is too long, the float is too small, or there is no more room in the int, but then you could just renumber the entity and make a fresh start. A scan through and update of all your records on a rare occasion is much better than faulting every object every time a user reorders.
当字符串太长、浮点数太小或 int 中没有更多空间时,可能会出现病理情况,但您可以重新编号实体并重新开始。在极少数情况下扫描并更新您的所有记录比每次用户重新订购时都检查每个对象要好得多。
For example, consider int32. Using the high 3 bytes as the initial ordering gives you almost 17 million rows with the ability to insert up to 256 rows between any two rows. 2 bytes allows inserting 65000 rows between any two rows before a rescan.
例如,考虑 int32。使用高 3 字节作为初始排序可以为您提供近 1700 万行,并且能够在任意两行之间插入多达 256 行。2 字节允许在重新扫描之前在任意两行之间插入 65000 行。
Here's the pseudo-code I have in mind for a 2 byte increment and 2 bytes for inserting:
这是我想到的 2 字节增量和 2 字节插入的伪代码:
AppendRow:item
item.sortKey = tail.sortKey + 0x10000
InsertRow:item betweenRow:a andNextRow:b
item.sortKey = a.sortKey + (b.sortKey - a.sortKey) >> 1
Normally you would be calling AppendRow resulting in rows with sortKeys of 0x10000, 0x20000, 0x30000, etc. Sometimes you would have to InsertRow, say between the first and the second, resulting in a sortKey of 0x180000.
通常你会调用 AppendRow 导致 sortKeys 为 0x10000、0x20000、0x30000 等的行。有时你必须插入行,比如在第一个和第二个之间,导致 sortKey 为 0x180000。
回答by Stephan
I have implemented the approach of @andrew / @dk with the the double values.
我已经用双值实现了@andrew / @dk 的方法。
You can find the UIOrderedTableViewon github.
您可以在 github 上找到UIOrderedTableView。
feel free to fork it :)
随意分叉它:)
回答by Arie Litovsky
I adapted this from method from Matt Gallagher's blog (can't find original link). This may not be the best solution if you have millions of records, but will defer saving until the user has finished reordering the records.
我根据 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];
}
回答by Andrew
Actually, there's a much simpler way, use a "double" type as an ordering column.
实际上,有一种更简单的方法,使用“double”类型作为排序列。
Then whenever you re-order you only EVER need to reset the value of the order attribute for the reordered item:
然后,每当您重新订购时,您只需要为重新订购的商品重置订单属性的值:
reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;
回答by Andrew
I finally gave up on FetchController in edit mode since I need to reorder my table cells as well. I'd like to see an example of it working. Instead I kept with having a mutablearray being the current view of the table, and also keeping the CoreData orderItem atrribute consistent.
我终于在编辑模式下放弃了 FetchController,因为我还需要重新排序我的表格单元格。我想看看它的工作示例。相反,我一直使用 mutablearray 作为表的当前视图,并保持 CoreData orderItem 属性一致。
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
}
}
}
If anyone has a solution using fetchController please post it.
如果有人有使用 fetchController 的解决方案,请发布它。
回答by user3519965
So having spent some time on this problem...!
所以在这个问题上花了一些时间......!
The answers above are great building blocks and without them I would have been lost, but as with other respondents I found that they only partially worked. If you implement them you will find that they work once or twice, then error, or you lose data as you go. The answer below is far from perfect - it's the result of quite a lot of late nights, trial and error.
上面的答案是很好的基石,没有它们我会迷失方向,但与其他受访者一样,我发现它们只是部分起作用。如果您实施它们,您会发现它们工作一两次,然后出错,或者您在执行过程中丢失数据。下面的答案远非完美——这是相当多的深夜、反复试验的结果。
There are some issues with these approaches:
这些方法存在一些问题:
The NSFetchedResultsController linked to NSMutableArray doesn't guarantee that the context will be updated, so you may see that this works sometimes, but not others.
The copy then delete approach for swapping objects is also difficult behaviour to predict. I found references elsewhere to unpredictable behaviour in referencing an object that had been deleted in the context.
If you use the object index row and have sections, then this won't behave properly. Some of the code above uses just the .row property and unfortunately this could refer to more than one row in a yt
Using NSFetchedResults Delegate = nil, is ok for simple applications, but consider that you want to use the delegate to capture changes that will be replicated to a database then you can see that this won't work properly.
Core Data doesn't really support sorting and ordering in the way that a proper SQL database does. The for loop solution above is good, but there should really be a proper way of ordering data - IOS8? - so you need to go into this expecting that your data will be all over the place.
链接到 NSMutableArray 的 NSFetchedResultsController 不保证上下文会被更新,所以你可能会看到这有时有效,但在其他情况下无效。
用于交换对象的复制然后删除方法也是难以预测的行为。我在其他地方找到了在引用上下文中已删除的对象时对不可预测行为的引用。
如果您使用对象索引行并有部分,那么这将无法正常运行。上面的一些代码只使用了 .row 属性,不幸的是这可能会引用 yt 中的多行
使用 NSFetchedResults Delegate = nil,对于简单的应用程序是可以的,但考虑到您想使用委托来捕获将复制到数据库的更改,然后您会发现这将无法正常工作。
Core Data 并不像正确的 SQL 数据库那样真正支持排序和排序。上面的 for 循环解决方案很好,但真的应该有一种正确的数据排序方式 - IOS8?- 所以你需要考虑到你的数据会到处都是。
The issues that people have posted in response to these posts relate to a lot of these issues.
人们为回应这些帖子而发布的问题与许多这些问题有关。
I have got a simple table app with sections to 'partially' work - there are still unexplained UI behaviours that I'm working on, but I believe that I have got to the bottom of it...
我有一个简单的表格应用程序,其中包含“部分”工作的部分 - 我仍在处理无法解释的 UI 行为,但我相信我已经深入了解了......
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
This is the usual delegate
这是通常的代表
{
userDrivenDataModelChange = YES;
uses the semaphore mechanism as described above with the if()return structures.
将上述信号量机制与 if() 返回结构一起使用。
NSInteger sourceRow = sourceIndexPath.row;
NSInteger sourceSection = sourceIndexPath.section;
NSInteger destinationRow = destinationIndexPath.row;
NSInteger destinationSection = destinationIndexPath.section;
Not all of these are used in the code, but it's useful to have them for debugging
并非所有这些都在代码中使用,但将它们用于调试很有用
NSError *error = nil;
NSIndexPath *destinationDummy;
int i = 0;
Final initialisation of variables
变量的最终初始化
destinationDummy = [NSIndexPath indexPathForRow:0 inSection:destinationSection] ;
// there should always be a row zero in every section - although it's not shown
I use a row 0 in each section that is hidden, this stores the section name. This allows the section to be visible, even when there are no 'live records in it. I use row 0 to get the section name. The code here is a bit untidy, but it does the job.
我在每个隐藏的部分中使用第 0 行,它存储部分名称。这允许该部分可见,即使其中没有“实时记录”。我使用第 0 行来获取部分名称。这里的代码有点乱,但它可以完成工作。
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSManagedObject *currentObject = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
NSManagedObject *targetObject = [self.fetchedResultsController objectAtIndexPath:destinationDummy];
Get the context and source and destination objects
获取上下文以及源和目标对象
This code then creates a new object which is takes the data from the source, and the section from the destination.
此代码然后创建一个新对象,该对象从源中获取数据,从目标中获取部分。
// 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"];
Now create a new object and save the context - this is cheating the move operation - you might not get the new record in exactly the place it was dropped - but at least it will be in the right section.
现在创建一个新对象并保存上下文——这是在欺骗移动操作——你可能无法在它被删除的地方获得新记录——但至少它会在正确的部分。
// 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();
}
Now do the for loop update as described above. Note that the context is saved before I do this - no idea why this is needed, but it didn't work properly when it wasn't!
现在执行上述的 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();
}
Set the semaphore back and update the table
重新设置信号量并更新表
userDrivenDataModelChange = NO;
[tableView reloadData];
}
}
回答by Todd Hoff
Here's what I'm doing that seems to work. For every entity I have a createDate that is used to sort the table by when it was created. It also acts as a unique key. So on the move all I do is swap the the source and destination dates.
这就是我正在做的似乎有效的事情。对于每个实体,我都有一个 createDate 用于按创建表的时间对表进行排序。它还充当唯一键。所以在移动中我所做的就是交换源日期和目标日期。
I would expect the table to be properly ordered after doing the saveContext, but what happens is the two cells just lay on top of each other. So I reload the data and the order is corrected. Starting the app from scratch shows the records still in the proper order.
我希望在执行 saveContext 后正确排序表格,但发生的情况是两个单元格只是彼此重叠。所以我重新加载数据并更正了顺序。从头开始启动应用程序显示的记录仍以正确的顺序排列。
Not sure it's a general solution or even correct, but so far it seems to work.
不确定这是一个通用的解决方案,甚至是正确的,但到目前为止它似乎有效。
- (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];
}
回答by Timothy Walters
Try having a look at the Core Data tutorial for iPhone here. One of the sections there talk about sorting (using NSSortDescriptor).
尝试在此处查看 iPhone 的 Core Data 教程。其中一节讨论了排序(使用 NSSortDescriptor)。
You may also find the Core Data basicspage to be useful.
您可能还会发现Core Data 基础知识页面很有用。