2009-07-03 81 views
60

我正在爲我的iPhone應用程序使用CoreData,但CoreData不提供允許您重新排序記錄的自動方式。我想使用另一列來存儲訂單信息,但使用連續數字排序索引有一個問題。如果我正在處理大量數據,重新排序記錄可能涉及更新訂購信息上的大量記錄(這就像改變數組元素的順序)如何實現CoreData記錄的重新排序?

實現高效排序方案的最佳方式是什麼?

+0

請參閱此處的示例代碼:http://stackoverflow.com/a/15625897/308315 – iwasrobbed 2013-03-27 01:01:47

回答

87

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

+1這是我實施文檔中提到的「changeIsUserDriven」的關鍵。謝謝。 – hanleyp 2010-01-14 04:23:38

+0

此外,請參閱http://stackoverflow.com/questions/1648223/how-can-i-maintain-display-order-in-uitableview-using-core-data/1648504#1648504 – gerry3 2010-01-28 21:11:18

0

請嘗試查看iPhone here的核心數據教程。其中一個部分討論了排序(使用NSSortDescriptor)。

您也許還會發現Core Data basics頁面有用。

+1

它不排序,但它是重新排序。排序很簡單,但重新排序會涉及潛在的大量更新,除非我使用雙鏈表方法,而我不知道如何將其轉換爲CoreData。 – Boon 2009-07-03 06:28:59

+0

是否有你不想用新的排序指令重新查詢的原因?它無論如何都訪問本地數據,所以它不像它是一個很大的性能影響。 – 2009-07-13 01:46:31

+0

這實際上不是關於避免排序。主要問題是,我如何實現排序呢?如果我在模式中添加順序索引,那麼當重新排序時,我會遇到必須進行大量重新計算的情況。試想一下,重新排列一個數組,你會明白我的意思 - 當你將一個數組元素移動到另一個位置時,它後面的所有內容都必須移動。 – Boon 2009-07-17 21:58:44

2

我終於放棄了在編輯模式下的FetchController,因爲我需要重新排序我的表格單元格。我希望看到它的一個例子。相反,我一直保留一個可變陣列作爲表的當前視圖,並保持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 
     }   

    }         

} 

如果任何人有使用fetchController的解決方案,請發佈它。

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 

通常你會被調用導致行與0x10000處,地址0x20000,0x30000等SORTKEYS AppendRow 。有時你必須在第一個和第二個之間插入InsertRow,導致sortKey爲0x180000。

2

實際上,有一種更簡單的方法,使用「double」類型作爲排序列。

然後,每當你重新排列你永遠只需要重置命令屬性的值的重新排序項:

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

我已經實現@andrew/@dk的方法與雙重價值。

你可以在github上找到UIOrderedTableView

隨意叉吧:)

5

我適應這個從馬特·加拉格爾的博客方法(無法找到原來的鏈接)。如果您擁有數百萬條記錄,這可能不是最佳解決方案,但會延遲保存,直到用戶完成對記錄的重新排序。

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

這裏是顯示的方式來轉儲獲取的成果變成一個NSMutableArray您可以使用它四處移動單元格一個簡單的例子。然後,您只需更新名爲orderInTable的實體的屬性,然後保存託管對象上下文。

這樣,您不必擔心手動更改索引,而是讓NSMutableArray爲您處理。

創建一個BOOL,你可以用它來暫時繞過NSFetchedResultsControllerDelegate

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

所以花了一些時間在這個問題上......!

上面的答案是很好的構建模塊,沒有它們我會丟失,但是和其他受訪者一樣,我發現他們只是部分工作。如果實施它們,您會發現它們會一次或兩次工作,然後出錯,或者隨時丟失數據。下面的答案遠非完美 - 這是相當多的深夜,反覆試驗的結果。

有一些問題,這些方法:

  1. 的NSFetchedResultsController鏈接到的NSMutableArray不保證範圍內將被更新,所以你可以看到,這個工作有時,但不是別人。

  2. 複製然後刪除方法交換對象也是難以預測的行爲。我在其他地方發現了引用在上下文中已被刪除的對象中的不可預知行爲的參考。

  3. 如果您使用對象索引行並且有段,那麼這將不會正常工作。上面的一些代碼只使用.row屬性,不幸的是這可能引用yt中的多行。

  4. 使用NSFetchedResults Delegate = nil對於簡單的應用程序是可以的,但考慮你想使用委託捕獲將被複制到數據庫的更改,然後您可以看到這將無法正常工作。

  5. 核心數據並不真正支持按適當的SQL數據庫的方式進行排序和排序。上面的for循環解決方案是好的,但真正應該有一個正確的方式來訂購數據 - IOS8? - 所以你需要進入這個期望,你的數據將遍佈整個地方。

人們針對這些帖子發佈的問題涉及到很多這些問題。

我有一個簡單的表格應用程序與部分以「部分的工作 - 仍然有我的工作原因不明的UI行爲,但我相信,我已經得到了它的底部...

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

}

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