2009-07-03 1 views
60

Sto usando CoreData per la mia app per iPhone, ma CoreData non fornisce un modo automatico per consentire di riordinare i record. Ho pensato di utilizzare un'altra colonna per memorizzare le informazioni dell'ordine, ma l'utilizzo di numeri contigui per l'indice di ordinamento ha un problema. se sto trattando un sacco di dati, riordinare un record potenzialmente comporta l'aggiornamento di molti record sulle informazioni di ordinazione (è un po 'come cambiare l'ordine di un elemento dell'array)Come implementare il riordino dei record CoreData?

Qual è il modo migliore per implementare uno schema di ordinazione efficiente?

+0

Vedi esempio di codice qui: http://stackoverflow.com/a/15625897/308315 – iwasrobbed

risposta

87

FetchedResultsController e il suo delegato non sono pensati per essere utilizzati per le modifiche del modello guidate dall'utente. Vedi the Apple reference doc. Cercare la parte Aggiornamenti guidati dall'utente. Quindi, se cerchi un modo magico, ad una sola riga, non c'è, purtroppo.

Quello che dovete fare è fare gli aggiornamenti in questo metodo:

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

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

userDrivenDataModelChange = NO; 
} 

e anche prevenire le notifiche di fare nulla, come i cambiamenti sono già fatto dall'utente:

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

ho l'ho appena implementato nella mia app to-do (Quickie) e funziona perfettamente.

+1

+1 Questa è stata la chiave per me per implementare il "changeIsUserDriven" menzionato nei documenti. Grazie. – hanleyp

+0

Inoltre, consulta http://stackoverflow.com/questions/1648223/how-can-i-maintain-display-order-in-uitableview-using-core-data/1648504#1648504 – gerry3

+3

Funziona alla grande, non dimenticarti di fare lo stesso per - (void) regolatore: (NSFetchedResultsController *) regolatore didChangeSection: (id ) sectionInfo atIndex: (NSUInteger) sectionIndex forChangeType: (NSFetchedResultsChangeType) tipo Se la tabella ha sezioni –

0

Prova a dare un'occhiata al tutorial di Core Data per iPhone here. Una delle sezioni parla dell'ordinamento (usando NSSortDescriptor).

La pagina Core Data basics può essere utile.

+1

Non è l'ordinamento però, è ri-ordinazione. L'ordinamento è semplice, ma il riordino comporta potenzialmente molti aggiornamenti, a meno che non utilizzi un approccio basato su un doppio elenco collegato, che non sono sicuro di come tradurre in CoreData. – Boon

+0

C'è un motivo per cui non vuoi ri-interrogare con una nuova istruzione di ordinamento? Sta comunque accedendo ai dati locali, quindi non è come se fosse un grande successo nelle prestazioni. –

+0

Non si tratta di evitare Sort. La domanda principale è, come posso implementare l'ordinamento in primo luogo? Se aggiungo l'indice degli ordini nello schema, mi imbatto nella situazione di dover ricalcolare molto quando ordino nuovamente una riga. Immagina questo con il riordino di un array e vedrai cosa intendo: quando sposti un elemento dell'array in un'altra posizione, anche tutto ciò che è successivo deve essere spostato. – Boon

2

Ho finalmente rinunciato a FetchController in modalità di modifica poiché ho bisogno di riordinare anche le celle del mio tavolo. Mi piacerebbe vedere un esempio di come funziona. Invece ho continuato a fare in modo che un mutablearray fosse la vista corrente della tabella, e anche mantenere l'ordinata di CoreData atrributa coerente.

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 
     }   

    }         

} 

Se qualcuno ha una soluzione utilizzando fetchController, si prega di postarlo.

8

Una risposta tardiva: forse è possibile memorizzare la chiave di ordinamento come una stringa. L'inserimento di un record tra due righe esistenti può essere fatto banalmente aggiungendo un carattere aggiuntivo a una stringa, ad es. inserendo "AM" tra le righe "A" e "B". Non è richiesto alcun riordino. Un'idea simile potrebbe essere ottenuta usando un numero in virgola mobile o qualche semplice aritmetica di bit su un intero di 4 byte: inserisci una riga con un valore di chiave di ordinamento che è a metà strada tra le righe adiacenti.

Si possono verificare casi patologici in cui la stringa è troppo lunga, il galleggiante è troppo piccolo o non c'è più spazio nell'int, ma è possibile rinumerare l'entità e ricominciare da capo. Una scansione e l'aggiornamento di tutti i tuoi record in una rara occasione è molto meglio di qualsiasi errore ogni volta che un utente riordina.

Ad esempio, considerare int32. Utilizzando i 3 byte alti, l'ordinamento iniziale offre quasi 17 milioni di righe con la possibilità di inserire fino a 256 righe tra due righe qualsiasi. 2 byte consente di inserire 65000 righe tra due file qualsiasi prima di una nuova scansione.

Ecco il pseudo-codice che ho in mente per un incremento di 2 byte e 2 byte per l'inserimento:

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

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

Normalmente si sarebbe chiamando AppendRow conseguente righe con sortKeys di 0x10000, 0x20000, 0x30000, ecc A volte dovresti inserire InsertRow, ad esempio tra il primo e il secondo, risultando in un sortKey di 0x180000.

2

In realtà, c'è un modo molto più semplice, utilizzare un tipo "doppio" come colonna di ordinamento.

Poi ogni volta che si ri-ordinare solamente mai bisogno di reimpostare il valore dell'attributo ordine per l'articolo riordinato:

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

Penso che devi distinguere tra le mosse su e giù e specialmente per l'indice 0 e l'ultimo oggetto. – Stephan

+0

Esegui anche il rischio di underflow a meno che non si normalizzi quando necessario. – MdaG

+0

double e float sono più lenti del numero intero durante il confronto. prova invece a usare l'intero. – flypig

5

ho implementato l'approccio di @andrew/@dk con i valori doppi .

È possibile trovare il UIOrderedTableView su github.

sentitevi liberi di sborsare it :)

+0

Buon lavoro, e grazie. – Ben

+0

Grazie ... bello vedere un feedback positivo – Stephan

5

mi sono adattato questo dal metodo dal blog di Matt Gallagher (non riesce a trovare link originale). Questa potrebbe non essere la soluzione migliore se hai milioni di record, ma differirai il salvataggio finché l'utente non avrà finito di riordinare i record.

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

Se l'utente si sposta più di una volta in un periodo di modifica, l'array allFRCObjects tornerà al suo stato originale all'inizio delle mosse consecutive. Pertanto, solo l'ultima mossa può essere salvata quando l'utente fa clic su Fine. – davidt

13

Ecco un breve esempio che mostra un modo per scaricare i risultati recuperati in un NSMutableArray che viene utilizzato per spostare le cellule intorno. Quindi devi semplicemente aggiornare un attributo sull'entità chiamata orderInTable e quindi salvare il contesto dell'oggetto gestito.

In questo modo, non è necessario preoccuparsi di modificare manualmente gli indici e lasciare che NSMutableArray lo gestisca automaticamente.

Creare un BOOL che è possibile utilizzare per bypassare temporaneamente il NSFetchedResultsControllerDelegate

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

Tabella metodo di visualizzazione delegato:

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

vostri delegati sarebbero simile a questa società:

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

Quindi, dopo aver dedicato un po 'di tempo a questo problema ...!

Le risposte di cui sopra sono ottimi mattoni e senza di loro sarei stato perso, ma come con altri intervistati ho scoperto che funzionavano solo parzialmente. Se li realizzi, scoprirai che funzionano una o due volte, poi un errore o perdi dati mentre vai. La risposta qui sotto è ben lungi dall'essere perfetta: è il risultato di molte nottate, prove ed errori.

Ci sono alcuni problemi con questi approcci:

  1. La NSFetchedResultsController legato alla NSMutableArray non garantisce che il contesto sarà aggiornato, così si può vedere che questo funziona a volte, ma non altri.

  2. La copia quindi elimina l'approccio per lo scambio di oggetti è anche un comportamento difficile da prevedere. Ho trovato riferimenti altrove a comportamenti imprevedibili nel fare riferimento a un oggetto che era stato eliminato nel contesto.

  3. Se si utilizza la riga di indice dell'oggetto e si dispone di sezioni, questo non si comporterà correttamente. Parte del codice sopra utilizza solo la proprietà .row e sfortunatamente questo potrebbe riferirsi a più di una riga in un

  4. Utilizzo di NSFetchedResults Delegato = nil, è ok per le applicazioni semplici, ma si consideri che si desidera utilizzare il delegato per acquisire le modifiche che verranno replicate in un database, è possibile vedere che questo non funzionerà correttamente.

  5. Core Data non supporta in realtà l'ordinamento e l'ordinamento nel modo in cui funziona un database SQL corretto. La soluzione loop for above è buona, ma dovrebbe esserci un modo corretto di ordinare i dati - IOS8? - Quindi devi entrare in questo aspetto aspettando che i tuoi dati saranno dappertutto.

I problemi che le persone hanno inviato in risposta a questi post riguardano molti di questi problemi.

ho ottenuto un tavolo semplice applicazione con le sezioni a 'parzialmente' lavoro - ci sono ancora comportamenti UI inspiegabili che sto lavorando, ma credo che ho avuto modo di fondo di esso ...

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

Questa è la solita delegato

{ 
userDrivenDataModelChange = YES; 

utilizza il meccanismo semaforo come descritto sopra con le strutture di ritorno, se().

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

tutti questi sono utilizzati nel codice, ma è utile avere loro per il debug

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

inizializzazione finale di variabili

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

Io uso una riga 0 in ogni sezione che è nascosta, memorizza il nome della sezione. Ciò consente alla sezione di essere visibile, anche quando non ci sono "registrazioni live". Io uso la riga 0 per ottenere il nome della sezione. Il codice qui è un po 'disordinato, ma fa il lavoro.

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

Prendi il contesto e la fonte e oggetti di destinazione

Questo codice crea un nuovo oggetto che è prende i dati dalla sorgente, e la sezione dalla destinazione.

// 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"]; 

Ora creare un nuovo oggetto e salvare il contesto - questo è barare l'operazione di spostamento - non si potrebbe ottenere il nuovo record esattamente il posto che è stata abbandonata - ma almeno sarà nella sezione giusta.

// 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(); 
} 

Ora eseguire l'aggiornamento del ciclo for come descritto sopra. Si noti che il contesto viene salvato prima di eseguire questa operazione, non so perché è necessario, ma non funzionava correttamente quando non lo era!

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

Impostare il semaforo indietro e aggiornare la tabella

userDrivenDataModelChange = NO; 

[tableView reloadData]; 

}

1

Ecco quello che sto facendo che sembra funzionare. Per ogni entità ho un createDate che viene utilizzato per ordinare la tabella quando è stata creata. Funziona anche come una chiave unica. Quindi in viaggio tutto ciò che faccio è scambiare le date di origine e destinazione.

Mi aspetto che la tabella venga ordinata correttamente dopo aver eseguito il saveContext, ma ciò che accade è che le due celle si sovrappongono l'una sull'altra. Quindi ricarico i dati e l'ordine è corretto. Avviare l'app da zero mostra i record ancora nell'ordine corretto.

Non sono sicuro che sia una soluzione generale o addirittura corretta, ma finora sembra funzionare.

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