2012-08-13 20 views
5

Estoy usando Grand Central Dispatch para cargar imágenes de UITableViewCell de forma asíncrona. Esto funciona bien, excepto en algunos casos fronterizos en los que se reutiliza la celda y un bloque anterior carga la imagen incorrecta.Reutilizando UITableViewCell con GCD

Mi código actual es el siguiente:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    static NSString *CellIdentifier = @"Cell"; 

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
    if (!cell) { 
     cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; 
    } 

    NSString *imagePath = [self imagePathForIndexPath:indexPath];   
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

    dispatch_async(queue, ^{ 
     UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

     dispatch_sync(dispatch_get_main_queue(), ^{ 
      cell.imageView.image = image; 
      [cell setNeedsLayout]; 
     }); 
    }); 

    return cell; 
} 

Por lo que yo sé colas GCD no se puede detener. ¿Cómo se puede prevenir este caso fronterizo? ¿O debería usar algo más en lugar de GCD para resolver este problema?

+0

Estoy usando GMGridView para mostrar una gran cantidad de miniaturas de imágenes y tener exactamente el mismo problema con ellas. También estoy usando GCD para cargar miniaturas de mi servidor web sobre la marcha. – Humayun

Respuesta

3

lo que he hecho se agrega un NSOperation ivar a la celda. la operación es responsable de obtener, cargar y crear la imagen. cuando se completa, hace estallar la celda. cuando/si la célula se cancela, la operación se cancela y se destruye si no se ha completado. la prueba de funcionamiento para la cancelación cuando está en -main. después de que la imagen se pasa a la celda, la celda elimina la operación y usa la imagen.

+1

NSOperation es seguramente mejor que GCD, usualmente lo utilizo, y ponerlo dentro de la celda es sin duda la mejor (y más elegante) forma ... +1 – meronix

+1

WWDC 2012 sesión 211 tiene un buen tutorial de esto también – jrturton

1

Puede tratar de forzar un reinicio de su imagen antes de iniciar su solicitud asincrónica.

Como usuario desplazarse la mesa, se puede ver la imagen de edad antes de que su método asíncrono cambia con la correcta

sólo tiene que añadir esta línea:

cell.imageView.image = nil; // or a placeHolder image 
dispatch_async(queue, ^{ 
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

    dispatch_sync(dispatch_get_main_queue(), ^{ 
     cell.imageView.image = image; 
     [cell setNeedsLayout]; 
    }); 
}); 

EDIT:

@hgpc:

No creo que esto resuelva el problema. Un bloque anterior aún puede establecer la imagen incorrecta antes de un nuevo bloque. - HGPC hace 9 minutos

Tienes razón, si de desplazamiento rápido de usuario esto puede ser un problema ...

la única manera que veo es hacer una celda personalizado con una propiedad mostrador donde almacenar (de una manera sync) el número de operationn en la cola de cada celda, y luego en el cheque método asíncrono que el contador es == 1 antes de cambiar la imagen:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
     static NSString *CellIdentifier = @"Cell"; 

     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
     if (!cell) { 
    // MyCustomUITableViewCell has a property counter 
      cell = [[[MyCustomUITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; 
      cell.counter = 0; 
     } 

     NSString *imagePath = [self imagePathForIndexPath:indexPath];   
     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

     cell.imageView.image = nil; // or a placeHolder image 
     cell.counter = cell.counter + 1; 
     dispatch_async(queue, ^{ 
      UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

      dispatch_sync(dispatch_get_main_queue(), ^{ 
       if (cell.counter == 1){ 
        cell.imageView.image = image; 
        [cell setNeedsLayout]; 
       } 
       cell.counter = cell.counter - 1; 

      }); 
     }); 

    return cell; 
} 
+0

No creo que esto resuelva el problema. Un bloque anterior aún puede establecer la imagen incorrecta antes de un nuevo bloque. – hpique

+0

derecha ... se la nueva edición en mi respuesta – meronix

+0

Como está ahora, el contador se asegura de que solo se establezca una imagen, pero no necesariamente la correcta. Basado en la sugerencia de @Willy, creo que simplemente podría establecer la etiqueta de vista con indexPath y comprobar si es la misma antes de cambiar la imagen. – hpique

0

puede establecer un identificador de célula antes iniciando la tarea asincrónica, y verifica que sea la misma antes de actualizar la UI.

+0

¿Por qué el voto a favor? Esto parece una buena idea. – hpique

0

Tiene un par de posibilidades aquí.

a) Use NSOperationQueue en lugar de GDC directo.
NSOperation se basa en GDC y habilitó una gran cantidad de administración, como detener hilos.
Eche un vistazo al concurrency guide de apple.

b) Utilice un cargador de imágenes listo para usar.
Hay suficiente proyecto de código abierto y gratuito disponible en internetz.
Yo personalmente recomiendo AFNetworking (particularmente la categoría AFNetworking+UIImageView) para eso.

Si quieres hacerlo solo (investigación, conocimiento, etc.)) debe quedarse con a), pero el método más conveniente es b).

actualización
acabo de notar, que está cargando la imagen de un archivo y no de la red.
En este caso, se debe manejar de manera diferente y cuidar a las siguientes puntos:

  • Evitar imageWithContentsOfFile: y más bien utilizar imageNamed:. Esto se debe a que imageNamed: almacena en caché las imágenes una vez que se cargaron y imageWithContentsOfFile: no lo hacen. Si necesita utilizar imageWithContentsOfFile: precargue todas las imágenes en una NSCache y rellene la tabla con imágenes de esta caché.

  • Las imágenes de TableView no deben ser de gran tamaño (dimensión y tamaño de archivo). Incluso una imagen de retina de 100x100 px no tiene más de un par de kb, lo que definitivamente no lleva tanto tiempo cargar que necesitaría una carga asíncrona.

+0

Debería tenga en cuenta que la imagen se está cargando desde un archivo, y ese es el desfase que quiero hacer asincrónico. Sin descarga involucrada. – hpique

+0

Ah, ya veo, sí. Pero en ese caso tienes otros problemas. Subiré mi respuesta en breve. – yinkou

+0

Así que actualicé mi respuesta. – yinkou

0

¿Qué hay de sólo cargar las imágenes cuando la tabla no se está desplazando? El problema con cellForRowWithIndexPath: es que estás haciendo mucho trabajo para las celdas que se están desplazando.

En lugar de cargar imágenes cada vez que se quitan las colas, puede hacerlo en viewDidLoad (o cada vez que se inicialice su modelo de datos) y en scrollViewDidEndDecelerating:.

-(void)viewDidLoad { 
    [super viewDidLoad]; 
    [self initializeData]; 
    [self loadVisibleImages]; 
} 

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 
    if (scrollView == self.tableView) { 
     [self loadVisibleImages]; 
    } 
} 

-(void)loadVisibleImages { 
    for (NSIndexPath *indexPath in [self.tableview indexPathsForVisibleRows]) { 
     dispatch_async(queue, ^{ 
      NSString *imagePath = [self imagePathForIndexPath:indexPath]; 
            UIImage *image = [UIImage imageWithContentsOfFile:imagePath];   
            dispatch_sync(dispatch_get_main_queue(), ^{ 
       UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; 
                cell.imageView.image = image; 
                [cell setNeedsLayout]; 
            }); 
        }); 
    } 
}