adding KVO to UITableViewCell

11,597

Solution 1

For background, you probably want to read the Key-Value Observing and Key-Value Coding Guides, if you haven't already. Then review the NSKeyValueObserving category methods.

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

In a nutshell, you need to carefully manage adding and removing the observing object to the observed objects list of observers (pardon the seeming redundancy of that statement). You don't want to have an object going away with observers still registered, or you get complaints and possible other issues.

That said, you use -addObserver:keyPath:options:context to add an object as an observer. Context should be a statically declared string. The options argument controls what data you get back in your observation method (see below). The keyPath is the path of property names from the observed object to the observed property (this may traverse multiple objects, and will be updated when intermediate objects change, not just when the leaf property changes).

In your case, you could observe the label, and use the text keyPath, or the cell, and use the nameLabel.text key path. If the table view class were designed differently, you might observe the entire array of cells, but there is no such property on UITableView. The problem with observing the cell is that the table view might delete it at any time (if your design uses multiple cells that serve the same purpose in a variable-length list). If you know your cells are static, you can probably observe them without worry.

Once you have an observer registered, that observer must implement -observeValueForKeyPath:ofObject:change:context:, confirm that the context matches (just compare the pointer value to your static string's address; otherwise, invoke super's implementation), then look into the change dictionary for the data you want (or just ask the object for it directly) and use it to update your model as you see fit.

There are many examples of KVO in sample code, including on Apple's developer site, and as part of the bindings samples on Malcolm Crawford (mmalc)'s site, but most of it is for Mac OS X, not iOS.

Solution 2

The above answer is great for static cells. Using KVO for UITableViewCells still works with cell reuse. Add the observers you need when the cell is about to appear, and remove them when the cell is no longer displayed. The only trick is that Apple seems to be inconsistent about sending didEndDisplayingCell:, so observers need to be removed in two places on 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];
    }
}

The following can occur if observers aren't cleaned up. The observer might try to notify whatever object is at that memory location, which may not even exist.

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

Solution 3

This works:

In configureCell:

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

In CustomCell:

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

Solution 4

In my case I added an observer to the custom cell label forKeyPath "text" with options (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld).

When observing the value for the keyPath I check to ensure the keyPath is the one I want, just as an extra measure and then I call my method for what ever operation I want to carry out on that label

e.g in my case

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

}
Share:
11,597
Z S
Author by

Z S

www.contactsjournal.com

Updated on June 30, 2022

Comments

  • Z S
    Z S almost 2 years

    I have a custom UITableViewCell which is displaying various attributes of a Person object (backed by Core Data) ... some labels, images etc. I currently force the whole tableview to reload whenever any property changes, and that's obviously not efficient. I know with KVO, I should be able to add a listener to a label in the cell that can listen for changes in the Person's properties. But I'm not sure how to implement it and can't find any examples.

    Here's what I typically do in my UITableView's cellForRowAtIndexPath:

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

    The cell is hooked up in IB. I would want to detect when displayName changes, and update just the name label. Thanks

  • Z S
    Z S over 12 years
    Think I got it, thanks to your help. Posted come code below, but will credit you with the answer.
  • Besi
    Besi almost 12 years
    When do you remove the observer and what about cell re-use?
  • Z S
    Z S almost 12 years
    In my case, I didn't want to remove the observers (since I'm adding the observer to the managed object, I'm guessing it should be whenever that's deleted or faulted?). For cell reuse, you might want to check if ([managedObject observationInfo] != nil) before adding the observer. I actually reworked my code after posting this so didn't end up using this solution at all; so I haven't worked all these details out.
  • Nikolai Ruhe
    Nikolai Ruhe almost 11 years
    Its an error to observe any key path not explicitly documented to be KVO compliant. You can't observe a label's text.
  • de.
    de. almost 11 years
    If I'm not mistaken, not the cell should be observed, but the Person object! Because the object might change and the cell should respond!
  • de.
    de. almost 11 years
    This works only for iOS 6.0+. Does anyone have a solution for iOS 5.1+?
  • djibouti33
    djibouti33 over 10 years
    very thorough, perfect!
  • Mojo66
    Mojo66 about 8 years
    I think KVO should be handled by the cell itself, and not the tableview.