ios - objective - swift 4 coredata
¿Cómo implementar el reordenamiento de los registros de CoreData? (10)
Estoy usando CoreData para mi aplicación de iPhone, pero CoreData no proporciona una forma automática de permitirle reordenar los registros. Pensé en usar otra columna para almacenar la información de la orden, pero el uso de números contiguos para ordenar el índice tiene un problema. si estoy lidiando con muchos datos, reordenar un registro implica potencialmente actualizar muchos registros en la información de pedido (es como cambiar el orden de un elemento de matriz)
¿Cuál es la mejor manera de implementar un esquema de pedido eficiente?
Aquí hay un ejemplo rápido que muestra una forma de volcar los resultados obtenidos en un NSMutableArray que usa para mover las celdas. Luego, simplemente actualiza un atributo en la entidad llamada orderInTable
y luego guarda el contexto del objeto administrado.
De esta forma, no tiene que preocuparse por cambiar manualmente los índices y, en su lugar, deja que NSMutableArray maneje eso por usted.
Cree un BOOL que pueda usar para omitir temporalmente NSFetchedResultsControllerDelegate
@interface PlaylistViewController ()
{
BOOL changingPlaylistOrder;
}
@end
Método de delegación de vista de tabla:
- (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;
}
Sus delegados se verían así ahora:
- (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];
}
En realidad, hay una manera mucho más simple, use un tipo "doble" como columna de pedido.
Luego, cada vez que reordena, NUNCA DEBE restablecer el valor del atributo de pedido para el artículo reordenado:
reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;
Entonces, ¡habiendo pasado un tiempo en este problema ...!
Las respuestas anteriores son grandes bloques de construcción y sin ellas me habría perdido, pero al igual que con otros encuestados, descubrí que solo funcionaban parcialmente. Si los implementa, encontrará que funcionan una o dos veces, luego un error o perderá datos a medida que avance. La respuesta a continuación dista mucho de ser perfecta: es el resultado de bastantes noches de retraso, de prueba y error.
Hay algunos problemas con estos enfoques:
NSFetchedResultsController vinculado a NSMutableArray no garantiza que el contexto se actualizará, por lo que puede ver que esto funciona a veces, pero no a otros.
El enfoque de copiar y luego eliminar para intercambiar objetos también es un comportamiento difícil de predecir. Encontré referencias en otros lugares al comportamiento impredecible al hacer referencia a un objeto que se había eliminado en el contexto.
Si usa la fila de índice de objeto y tiene secciones, entonces esto no se comportará correctamente. Parte del código anterior usa solo la propiedad .row y, lamentablemente, esto podría referirse a más de una fila en yt
Usando NSFetchedResults Delegate = nil, está bien para aplicaciones simples, pero considere que desea usar el delegado para capturar los cambios que se replicarán en una base de datos, entonces puede ver que esto no funcionará correctamente.
Core Data realmente no es compatible con la ordenación y el ordenamiento en la forma en que lo hace una base de datos SQL adecuada. La solución de bucle for anterior es buena, pero realmente debería haber una forma adecuada de ordenar datos: ¿IOS8? - entonces necesitas entrar en esto esperando que tus datos estén por todos lados.
Los problemas que las personas han publicado en respuesta a estas publicaciones se relacionan con muchos de estos problemas.
Tengo una aplicación de tabla simple con secciones para trabajar ''parcialmente''. Aún hay comportamientos de IU inexplicables en los que estoy trabajando, pero creo que ya llegué al fondo ...
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
Este es el delegado habitual
{
userDrivenDataModelChange = YES;
usa el mecanismo de semáforo como se describió anteriormente con las estructuras de retorno if ().
NSInteger sourceRow = sourceIndexPath.row;
NSInteger sourceSection = sourceIndexPath.section;
NSInteger destinationRow = destinationIndexPath.row;
NSInteger destinationSection = destinationIndexPath.section;
No todos estos se usan en el código, pero es útil tenerlos para la depuración
NSError *error = nil;
NSIndexPath *destinationDummy;
int i = 0;
Inicialización final de variables
destinationDummy = [NSIndexPath indexPathForRow:0 inSection:destinationSection] ;
// there should always be a row zero in every section - although it''s not shown
Utilizo una fila 0 en cada sección que está oculta, esto almacena el nombre de la sección. Esto permite que la sección sea visible, incluso cuando no haya ''registros en vivo''. Utilizo la fila 0 para obtener el nombre de la sección. El código aquí es un poco desordenado, pero cumple su función.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSManagedObject *currentObject = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
NSManagedObject *targetObject = [self.fetchedResultsController objectAtIndexPath:destinationDummy];
Obtenga el contexto y los objetos de origen y destino
Este código crea un nuevo objeto que toma los datos del origen y la sección del destino.
// 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"];
Ahora cree un nuevo objeto y guarde el contexto; esto es hacer trampa en la operación de mover; es posible que no obtenga el nuevo registro exactamente en el lugar donde lo dejó, pero al menos estará en la sección correcta.
// 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();
}
Ahora haz la actualización for loop como se describe arriba. Tenga en cuenta que el contexto se guarda antes de hacer esto, no tengo idea de por qué es necesario, pero ¡no funcionó correctamente cuando no 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();
}
Restablece el semáforo y actualiza la tabla
userDrivenDataModelChange = NO;
[tableView reloadData];
}
Esto es lo que estoy haciendo que parece funcionar. Para cada entidad tengo un createDate que se usa para ordenar la tabla por cuando fue creada. También actúa como una clave única. Así que en movimiento todo lo que hago es cambiar las fechas de origen y destino.
Esperaría que la tabla se ordene correctamente después de guardar el SaveContext, pero lo que sucede es que las dos celdas se colocan una sobre otra. Así que recargo los datos y se corrige la orden. Iniciar la aplicación desde cero muestra los registros aún en el orden correcto.
No estoy seguro de que sea una solución general o incluso correcta, pero hasta ahora parece funcionar.
- (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];
}
FetchedResultsController y su delegado no están destinados a ser utilizados para cambios de modelo impulsados por el usuario. Ver el documento de referencia de Apple . Busque la parte de Actualizaciones dirigidas por el usuario. Entonces, si buscas una forma mágica de una sola línea, no hay tal cosa, lamentablemente.
Lo que debes hacer es actualizar en este método:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
userDrivenDataModelChange = YES;
...[UPDATE THE MODEL then SAVE CONTEXT]...
userDrivenDataModelChange = NO;
}
y también evita que las notificaciones hagan algo, ya que el usuario ya ha realizado cambios:
- (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;
...
}
Acabo de implementar esto en mi aplicación de tareas (Quickie) y funciona bien.
Finalmente me rendí en FetchController en modo de edición ya que también necesito reordenar las celdas de mi tabla. Me gustaría ver un ejemplo de que funcione. En cambio, seguí teniendo un mutablearray que era la vista actual de la tabla, y también mantenía el orden de artículo de CoreData atrribute consistente.
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
}
}
}
Si alguien tiene una solución usando fetchController, publícala.
Implementé el enfoque de @andrew / @dk con los valores dobles.
Puede encontrar UIOrderedTableView en github.
no dudes en bifurcarlo :)
Intenta echar un vistazo al tutorial de Core Data para iPhone here . Una de las secciones habla de clasificación (usando NSSortDescriptor).
También puede encontrar que la página básica de Datos básicos es útil.
Lo adapté del método del blog de Matt Gallagher (no puedo encontrar el enlace original). Puede que esta no sea la mejor solución si tiene millones de registros, pero postergará el almacenamiento hasta que el usuario haya terminado de reordenar los registros.
- (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];
}
Una respuesta tardía: quizás podría guardar la clave de clasificación como una cadena. Insertar un registro entre dos filas existentes se puede hacer trivialmente agregando un carácter adicional a una cadena, por ejemplo, insertando "AM" entre las filas "A" y "B". No se requiere reordenamiento. Se podría lograr una idea similar utilizando un número de punto flotante o una aritmética de bits simple en un entero de 4 bytes: inserte una fila con un valor de clave de clasificación que esté a medio camino entre las filas adyacentes.
Podrían surgir casos patológicos en los que el hilo sea demasiado largo, el flotador sea demasiado pequeño o no haya más espacio en el int, pero luego podría renumerar la entidad y comenzar de nuevo. Un escaneo y actualización de todos sus registros en una rara ocasión es mucho mejor que fallar cada objeto cada vez que un usuario realiza una nueva orden.
Por ejemplo, considere int32. El uso de los 3 bytes altos como el orden inicial le da casi 17 millones de filas con la capacidad de insertar hasta 256 filas entre dos filas. 2 bytes permite insertar 65000 filas entre dos filas antes de volver a escanear.
Aquí está el pseudo-código que tengo en mente para un incremento de 2 bytes y 2 bytes para insertar:
AppendRow:item
item.sortKey = tail.sortKey + 0x10000
InsertRow:item betweenRow:a andNextRow:b
item.sortKey = a.sortKey + (b.sortKey - a.sortKey) >> 1
Normalmente estaría llamando a AppendRow dando como resultado filas con sortKeys de 0x10000, 0x20000, 0x30000, etc. Algunas veces tendría que InsertRow, por ejemplo, entre la primera y la segunda, lo que da como resultado una sortKey de 0x180000.