2011-10-05 31 views
11

Tengo una UITableViewCell personalizada que muestra varios atributos de un objeto Person (respaldado por datos centrales) ... algunas etiquetas, imágenes, etc. Actualmente fuerzo a toda la tabla para volver a cargar cada vez que cambia alguna propiedad, y eso obviamente no es eficiente. Sé con KVO que debería poder agregar un oyente a una etiqueta en la celda que pueda escuchar los cambios en las propiedades de la persona. Pero no estoy seguro de cómo implementarlo y no puedo encontrar ningún ejemplo.agregando KVO a UITableViewCell

Esto es lo que normalmente hago en cellForRowAtIndexPath de mi UITableView:

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

     CustomCell *cell = (CustomCell *) [tableView dequeueReusableCellWithIdentifier:simple]; 

     if (cell == nil) 
     { 
      NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil]; 

      for (id findCell in nib) 
      { 
       if ([findCell isKindOfClass: [CustomCell class]]) 
       { 
        cell = findCell; 
       }  
      } 
     } 
     Person *managedObject = [self.someArray objectAtIndex: indexPath.row]; 
     cell.namelabel.text = managedObject.displayName; 
     return cell; 
} 

La célula está conectado en IB. Me gustaría detectar cuándo cambia el día y actualizar solo la etiqueta del nombre. Gracias

Respuesta

5

Para obtener más información, probablemente desee leer las Guías de codificación de Observación de valores-clave y valor-clave, si aún no lo ha hecho. Luego revise los métodos de categoría NSKeyValueObserving.

http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueObserving_Protocol/Reference/Reference.html

En pocas palabras, tiene que manejar con cuidado agregar y quitar el objeto de observar a la lista de objetos observados de los observadores (perdón por la redundancia aparente de esta afirmación). No desea que un objeto se vaya con observadores aún registrados, o recibe quejas y otros posibles problemas.

Dicho esto, usa -addObserver:keyPath:options:context para agregar un objeto como observador. El contexto debe ser una cadena estáticamente declarada. El argumento de opciones controla qué datos obtiene en su método de observación (ver a continuación). KeyPath es la ruta de acceso de los nombres de propiedades desde el objeto observado a la propiedad observada (esto puede atravesar múltiples objetos, y se actualizará cuando cambien los objetos intermedios, no solo cuando cambia la propiedad de la hoja).

En su caso, podría observar la etiqueta, y usar el text keyPath, o la celda, y usar la ruta de la clave nameLabel.text. Si la clase de vista de tabla se diseñó de manera diferente, es posible que observe la matriz completa de celdas, pero no existe dicha propiedad en UITableView. El problema con la observación de la celda es que la vista de tabla podría eliminarla en cualquier momento (si su diseño usa varias celdas que tienen el mismo propósito en una lista de longitud variable). Si sabes que tus células son estáticas, probablemente puedas observarlas sin preocupaciones.

Una vez que tenga un observador registrado, que observador debe aplicar -observeValueForKeyPath:ofObject:change:context:, confirme que el contexto coincide (justo comparar el valor del puntero a la dirección de su cadena estática, de lo contrario, invocar la aplicación del súper), y luego buscar en el diccionario de cambio para el datos que desea (o simplemente solicite el objeto directamente) y úselos para actualizar su modelo como mejor le parezca.

Hay muchos ejemplos de KVO en código de muestra, incluido en el sitio de desarrollador de Apple, y como parte de los ejemplos de enlaces en el sitio de Malcolm Crawford (mmalc), pero la mayoría es para Mac OS X, no para iOS.

+0

Creo que lo tengo, gracias a su ayuda. Publica el código a continuación, pero te acreditará con la respuesta. –

+0

Es un error observar cualquier ruta clave no documentada explícitamente para ser compatible con KVO. No puedes observar el 'texto' de una etiqueta. –

+0

Si no me equivoco, no se debe observar la 'celda', ¡sino el objeto' Persona'! ¡Porque el objeto podría cambiar y la célula debería responder! –

2

Esto funciona:

En configureCell:

[managedObject addObserver: cell forKeyPath: @"displayName" options:NSKeyValueObservingOptionNew context: @"Context"]; 

En CustomCell:

- (void)observeValueForKeyPath:(NSString *)keyPath 
         ofObject:(id)object 
         change:(NSDictionary *)change 
         context:(void *)context 
{ 
    Person *label = (Person *) object; 
    self.namelabel.text = [label valueForKey:@"displayName"]; 
} 
+8

¿Cuándo quita el observador y qué pasa con la reutilización de células? – Besi

+0

En mi caso, no quise eliminar a los observadores (dado que estoy agregando el observador al objeto administrado, supongo que debería ser siempre que se elimine o tenga una falla). Para la reutilización de células, es posible que desee comprobar si ([managedObject observationInfo]! = Nil) antes de agregar el observador.De hecho, rehice mi código después de publicar esto, así que no terminé usando esta solución; así que no he trabajado todos estos detalles. –

18

La respuesta anterior es ideal para las células estáticas. El uso de KVO para UITableViewCell s aún funciona con la reutilización de células.Agregue los observadores que necesita cuando la celda está por aparecer, y elimínelos cuando la celda ya no se muestre. El único truco es que Apple parece ser inconsistente sobre el envío didEndDisplayingCell :, por lo que los observadores deben ser eliminados en dos lugares en IOS 6,1

@implementation MyTableViewCell 

@property MyTableViewController * __weak parentTVC; 

- (UITableViewCell *)tableView:(UITableView *)tableView 
cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    ((MyTableViewCell *)cell).parentTVC = self; 
    // Don't add observers, or the app may crash later when cells are recycled 
} 


- (void)tableView:(UITableView *)tableView 
    willDisplayCell:(HKTimelineCell *)cell 
forRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    // Add observers 
} 

- (void)tableView:(UITableView *)tableView 
didEndDisplayingCell:(UITableViewCell *)cell 
forRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    [self removeMyKVOObservers]; 
} 

- (void)viewWillDisappear:(BOOL)animated 
{ 
    for (MyTableViewCell *cell in self.visibleCells) { 
     // note! didEndDisplayingCell: isn't sent when the entire controller is going away! 
     [self removeMyKVOObservers]; 
    } 
} 

puede ocurrir si los observadores no se limpian el siguiente. El observador puede intentar notificar cualquier objeto que se encuentre en esa ubicación de memoria, que puede que ni siquiera exista.

<NSKeyValueObservationInfo 0x1d6e4860> ( <NSKeyValueObservance 0x1d4ea9f0: Observer: 0x1d6c9540, Key path: someKeyPath, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x1c5c7e60> <NSKeyValueObservance 0x1d1bff10: Observer: 0x1d6c9540, Key path: someOtherKeyPath, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x1c588290>)

+0

Esto funciona solo para iOS 6.0+. ¿Alguien tiene una solución para iOS 5.1+? –

+0

muy completo, perfecto! – djibouti33

+0

explicación clara ... gracias –

1

En mi caso he añadido un observador a la etiqueta de celda personalizado forKeyPath "texto" con opciones (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld).

Al observar el valor de la keyPath puedo comprobar para asegurar la keyPath es el que yo quiero, al igual que una medida adicional y luego llamo a mi método para lo que cada vez la operación Quiero llevar a cabo en esa etiqueta

por ejemplo, en mi caso

-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 
{ 
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 

    if (self) { 
     // Helpers 
     CGSize cellSize = self.contentView.frame.size; 

     CGRect sizerFrame = CGRectZero; 
     sizerFrame.origin.x = kDefaultUITableViewCellContentLeftInset; 
     sizerFrame.origin.y = kDefaultUITableViewCellContentTopInset; 

     // The Profile Image 
     CGRect imageFrame = CGRectMake(sizerFrame.origin.x, sizerFrame.origin.y, kDefaultProfilePictureSizeBWidth, kDefaultProfilePictureSizeBHeight); 
     self.userProfilePictureUIImageView = [[UIImageView alloc] initWithFrame:imageFrame]; 
     [self.userProfilePictureUIImageView setImage:[UIImage imageNamed:@"placeholderImage"]]; 
     [ApplicationUtilities formatViewLayer:self.userProfilePictureUIImageView withBorderRadius:4.0]; 

     // adjust the image content mode based on the lenght of it's sides 
     CGSize avatarSize = self.userProfilePictureUIImageView.image.size; 

     if (avatarSize.width < avatarSize.height) { 
      [self.userProfilePictureUIImageView setContentMode:UIViewContentModeScaleAspectFill]; 
     } else { 
      [self.userProfilePictureUIImageView setContentMode:UIViewContentModeScaleAspectFit]; 
     } 

     CGFloat readStateSize = 10.0; 
     CGRect readStateFrame = CGRectMake((imageFrame.origin.x + imageFrame.size.width) - readStateSize, CGRectGetMaxY(imageFrame) + 4, readStateSize, readStateSize); 

     // Read State 
     self.readStateUIImageView = [[UIImageView alloc] initWithFrame:readStateFrame]; 
     self.readStateUIImageView.backgroundColor = RGBA2UIColor(0.0, 157.0, 255.0, 1.0); 
     [ApplicationUtilities formatViewLayer:self.readStateUIImageView withBorderRadius:readStateSize/2]; 


     sizerFrame.origin.x = CGRectGetMaxX(imageFrame) + kDefaultViewContentHorizontalSpacing; 
     // read just the width of the senders label based on the width of the message label 
     CGRect messageLabelFrame = sizerFrame; 
     messageLabelFrame.size.width = cellSize.width - (CGRectGetMinX(messageLabelFrame) + kDefaultViewContentHorizontalSpacing); 
     messageLabelFrame.size.height = kDefaultInitialUILabelHeight; 

     // Store the original frame for resizing 
     initialLabelFrame = messageLabelFrame; 

     self.messageLabel = [[UILabel alloc]initWithFrame:messageLabelFrame]; 
     [self.messageLabel setBackgroundColor:[UIColor clearColor]]; 
     [self.messageLabel setFont:[UIFont systemFontOfSize:14.0]]; 
     [self.messageLabel setTextColor:[UIColor blackColor]]; 
     [self.messageLabel setNumberOfLines:2]; 
     [self.messageLabel setText:@""]; 

     // Modify Sizer Frame for Message Date Label 
     sizerFrame = initialLabelFrame; 
     // Modify the y offset 
     sizerFrame.origin.y = CGRectGetMaxY(sizerFrame) + kDefaultViewContentVerticalSpacing; 

     // Message Date 
     self.messageDateLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 
     [self.messageDateLabel setBackgroundColor:[UIColor clearColor]]; 
     [self.messageDateLabel setFont:[UIFont systemFontOfSize:12.0]]; 
     [self.messageDateLabel setTextColor:RGBA2UIColor(200.0, 200.0, 200.0, 1.0)]; 
     [self.messageDateLabel setHighlightedTextColor:[UIColor whiteColor]]; 
     [self.messageDateLabel setTextAlignment:NSTextAlignmentRight]; 
     [self.messageDateLabel setNumberOfLines:1]; 
     [self.messageDateLabel setText:@"Message Date"]; 
     [self.messageDateLabel sizeToFit]; 

     [self.contentView addSubview:self.userProfilePictureUIImageView]; 
     [self.contentView addSubview:self.readStateUIImageView]; 
     [self.contentView addSubview:self.messageDateLabel]; 
     [self.contentView addSubview:self.messageLabel]; 

     // Add KVO for all text labels 
     [self.messageDateLabel addObserver:self forKeyPath:@"text" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL]; 
     [self.messageLabel addObserver:self forKeyPath:@"text" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL]; 

    } 
    return self; 
} 

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{ 
    if ([keyPath isEqual:@"text"]) { 

     [self resizeCellObjects]; 
    } 
} 

-(void)resizeCellObjects 
{ 
    // Resize and reposition the message label 
    CGRect messageLabelFrame = initialLabelFrame; 

    self.messageLabel.frame = messageLabelFrame; 
    [self.messageLabel setNumberOfLines:2]; 
    [self.messageLabel sizeToFit]; 

    // Resize the messageDate label 
    CGRect messageDateFrame = initialLabelFrame; 
    messageDateFrame.origin.y = CGRectGetMaxY(self.messageLabel.frame) + kDefaultViewContentVerticalSpacing; 
    self.messageDateLabel.frame = messageDateFrame; 

    [self.messageDateLabel sizeToFit]; 

} 
0

que prefieren una solución en la que el UITableViewCell hace todo el MVA por su propia cuenta. Mi configuración es la siguiente:

En mi subclase de celda, tengo una propiedad que guarda una fuerte referencia a mi clase de modelo de la cual recupero mis datos, y un método al que llamo cuando quiero adjuntar un nuevo objeto a la propiedad:

@interface MyTableViewCell : UITableViewCell 

@property (atomic) id object; 
- (void)populateFromObject:(id)object; 

Implementación:

- (void)awakeFromNib { 
[super awakeFromNib]; 
self.contentView.hidden = YES;// avoid displaying an unpopulated cell 
} 

- (void)populateFromObject:(id)object { 
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),^{// handle KVO on a bg thread 
     if (object && (self.object != object)) {// if new object differs from property... 
      [self unregisterFromKVO];// ...unregister from old object and... 
      self.object = object; 
      for (NSString *keyToObserve in [[object class] displayKeys]) {// ...register to new object 
       [object addObserver:self forKeyPath:keyToObserve options:0 context:nil]; 
      } 
     } 
    }); 
    dispatch_async(dispatch_get_main_queue(), ^{// UI updates on main thread only 
     // update your outlets here 
     self.contentView.hidden  = NO;// finally display the cell now that it is properly populated 
    }); 
} 



// =========== 
#pragma mark - KVO 
// =========== 

// KVO notification 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { 
    [self populateFromObject:object]; 
} 

- (void)unregisterFromKVO { 
     for (NSString *keyToObserve in [YourModelObject displayKeys]) { 
      [self.object removeObserver:self forKeyPath:keyToObserve]; 
     } 
} 

- (void)dealloc { 
    [self unregisterFromKVO]; 
} 

Tenga en cuenta que el MVA real se maneja en un subproceso de fondo para evitar bloquear el hilo principal durante el desplazamiento. También tenga en cuenta que regresa inmediatamente y, por lo tanto, mostraría una celda despoblada. Para evitar esto, ocultamos la vista de contenido hasta que la celda esté completamente poblada. Ahora lo único que queda es poner en práctica un método de clase en YourModelObject que devuelve una matriz de las claves que desea KVO:

+ (NSArray<NSString *> *)displayKeys { 
    return @[@"name",@"Street", @"ZipCode"]; 
} 

..y en UITableViewController:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
MyTableViewCell *cell  = [tableView dequeueReusableCellWithIdentifier:@"reuseid" forIndexPath:indexPath]; 
YourModelObject *obj = [myModelArray objectAtIndex:indexPath.row]; 
[cell populateFromObject:obj]; 

return cell; 
} 

La fuerte referencia a partir de la celda del objeto modelo asegura que el objeto no se desasignará mientras la celda aún observe una de sus propiedades, es decir, que sea visible. Una vez que la celda es desasignada, el KVO no está registrado y solo entonces el objeto modelo será desasignado. Por conveniencia, también tengo una referencia débil del objeto modelo a la celda que puede ser útil al implementar los métodos delegados UITableView.

+0

El método -populateFromObject: despacha dos tareas llamando a dispatch_async() dos veces. Las dos tareas se envían a diferentes colas. Si no me equivoco, no hay garantía de que la segunda tarea se ejecutará después de que la primera haya finalizado, por lo que la afirmación de que "ocultamos la vista de contenido hasta que la celda esté completamente poblada" no es del todo exacta. –