cocoa touch - awesome - ¿Cómo actualizo eficientemente un UITableView con animación?
skeleton swift (2)
La aplicación de mi iPad cuenta con un UITableView rellenado desde una fuente. Como la mayoría de los lectores de RSS, muestra una lista de enlaces a publicaciones de blog en orden cronológico inverso, con sus títulos y un resumen de cada publicación. El feed se actualiza con frecuencia, y es bastante grande, alrededor de 500 publicaciones. Estoy usando el análisis de envío libxml2 para descargar y analizar de manera eficiente la fuente en una subclase NSOperation, construyendo objetos de entrada y actualizando una base de datos a medida que avanzo. Pero luego necesito actualizar el UITableView con los cambios.
Hasta ahora, la aplicación ha estado actualizando el UITableView para cada publicación analizada, a medida que se analiza. El analizador realiza un selector en el subproceso principal para realizar este trabajo. Pero esto lleva a un retraso serio durante un par de segundos si es necesario actualizar muchas celdas. Puedo mitigar esto ejecutando la actualización de la tabla en un subproceso en segundo plano, pero parece que no es una buena idea . Así que ahora estoy tratando de averiguar cómo actualizar la tabla de manera más eficiente en el hilo principal.
Podría simplemente llamar a reloadData
cuando se hayan analizado todas las publicaciones, pero no es muy fácil de usar: no hay animación que indique que algo haya cambiado, solo un flash y los nuevos datos están ahí. Preferiría tenerlo animado para mostrar que se agregan nuevas publicaciones y se eliminan las antiguas. Las publicaciones existentes que no se eliminan de la fuente deben ser empujadas hacia abajo en la tabla por las nuevas publicaciones que aparecen en la parte superior.
Sé que esto es posible. Byline , para dar un ejemplo, hace un trabajo hermoso. Cada publicación se agrega o se elimina de UITableView una a la vez, sin espacios que muestren el fondo de la tabla. Todo sin hacer que la interfaz de usuario en lo más mínimo no responda. ¿Cómo se hace eso?
Mi último intento es actualizar la tabla solo después de que se hayan analizado todas las publicaciones (el analizador es bastante rápido, por lo que no es un gran retraso). A continuación, carga las publicaciones existentes en un NSDictionary que asigna sus ID a sus índices en la matriz utilizada como fuente de datos de la tabla. Luego itera sobre cada objeto en el conjunto de publicaciones recientemente analizadas, agregando NSIndexPath para cada una de las -deleteRowsAtIndexPaths:withRowAnimation:
, y luego se pasa a -insertRowsAtIndexPaths:withRowAnimation:
-deleteRowsAtIndexPaths:withRowAnimation:
, mover, o actualizar celdas. Para 500 publicaciones, esto tarda alrededor de 4 segundos en actualizarse, con la interfaz de usuario completamente sin respuesta. Ese tiempo se usa casi exclusivamente para las actualizaciones animadas de UITableView; la iteración de las dos matrices de publicaciones lleva muy poco tiempo.
Luego lo modifiqué para que se actualicen sin animación , y tengo matrices separadas para insertar / eliminar / recargar con animación solo para las posiciones de fila correspondientes a las filas actualmente visibles. Esto es mejor, pero las brechas aparecen a medida que se eliminan las publicaciones y se agregan otras nuevas.
Lo siento, esto es tan largo, pero aquí está el resultado:
¿Cómo puedo actualizar un UITableView, con nuevas celdas activadas, otras rechazadas y aún otras movidas de una posición a otra, con hasta 500 celdas en la UITableView (6-8 son visibles al mismo tiempo), y cada animación ocurre? en secuencia, todo mientras la interfaz de usuario permanece completamente sensible?
¿Has probado [tableView beginUpdates];
y [tableView endUpdate];
?
Esta pregunta en realidad tiene tres respuestas. Es decir, hay tres partes en esta pregunta:
- Cómo mantener la interfaz de usuario sensible
- Cómo mantener la actualización rápida
- Cómo hacer que la actualización de la tabla sea fluida
La capacidad de respuesta de la interfaz de usuario
Para resolver el primer problema, ahora me aseguro de que no se pueda entregar más de un mensaje de actualización de tabla en cada iteración del bucle de eventos principal. Eso evita que el hilo principal se bloquee si el hilo de fondo lo alimenta para que sea más rápido de lo que puede hacer frente a él.
Esto se hace gracias al código de ejemplo que me envió el autor de Byline , Milo Bird, que luego integré en DDInvocationGrabber de Dave DDInvocationGrabber . Esta interfaz hace que sea muy fácil poner en cola un método para invocarlo en la próxima iteración disponible del bucle de eventos principal:
[[(id)delegate queueOnMainThread]
parserParsedEntries:parsedEntries
inPortal:parsedPortal];
Me gusta lo fácil que es usar este método. El analizador ahora lo usa para llamar a todos los métodos de delegado, la mayoría de los cuales actualizan la interfaz de usuario. He lanzado este código en GitHub .
Actuación
En cuanto al rendimiento, originalmente estaba actualizando una fila UITableView a la vez. Esto fue efectivo, pero algo ineficiente. XMLPerformance y estudié el ejemplo de XMLPerformance , donde noté que el analizador estaba esperando hasta que hubiera recopilado 10 elementos antes de enviarlo al hilo principal para actualizar la tabla. Esto fue clave para mantener el rendimiento sin hacer que la IU se bloquee al actualizar las 500 filas a la vez. Jugué un poco con la actualización de 1, 10 y las 500 filas en una sola llamada, y la actualización de 10 parecía ofrecer la mejor relación entre el rendimiento y el bloqueo de la interfaz de usuario. cinco probablemente funcionaría bastante bien, también.
Animación
Y finalmente, está la animación. Al ver la sesión WWDC 2010 de “Mastering Table Views” , me di cuenta de que utilizaba deleteRowsAtIndexPaths:withRowAnimation:
y updateRowsAtIndexPaths:withRowAnimation:
métodos eran incorrectos. Había estado haciendo un seguimiento de dónde deberían agregarse y eliminarse las cosas en la tabla y ajustar los índices según fuera apropiado, pero resulta que no es necesario. Dentro de un bloque de actualización de la tabla, solo se necesita hacer referencia al índice de una fila anterior a la actualización, independientemente de la cantidad que se pueda insertar o eliminar para cambiar su posición. El bloque de actualización, al parecer, hace toda esa contabilidad para usted. (Vaya a la marca de las 8:45 en el video para ver el ejemplo clave).
Por lo tanto, el método de delegado que actualiza la tabla para el número de entradas que le pasa el analizador (actualmente 10 a la vez) ahora realiza un seguimiento explícito de las posiciones de las filas que se actualizarán o eliminarán antes del bloque de actualización, por lo que :
NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in posts) {
[oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}
NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;
for (PostModel *entry in entries) {
NSNumber *num = [oldIndexFor objectForKey:entry.ident];
NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
if (num == nil) {
modified = YES;
[insertPaths addObject:path];
[posts insertObject:entry atIndex:currentPostIndex];
} else {
// Find its current position in the array.
NSUInteger foundAt = [posts indexOfObject:entry];
if (foundAt == currentPostIndex) {
// Reload it if it has changed.
if (entry.savedState != PostModelSavedStateUnmodified) {
modified = YES;
[posts replaceObjectAtIndex:foundAt withObject:entry];
[reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
} else {
// Move it.
modified = YES;
[posts removeObjectAtIndex:foundAt];
[posts insertObject:entry atIndex:currentPostIndex];
[insertPaths addObject:path];
[deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
}
currentPostIndex++;
}
if (modified) {
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
[tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
[tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
Comentarios bienvenidos. Es completamente posible que haya formas más eficientes de hacer esto (el uso de -[NSArray indexOfObject:]
es particularmente sospechoso para mí), y es posible que me haya perdido alguna otra sutileza.
Pero aún así, esta es una gran mejora para mi aplicación. La interfaz de usuario ahora permanece (en su mayoría) receptiva durante una sincronización, la sincronización es rápida y la animación de actualización de la tabla se ve bien.