Добавление KVO в UITableViewCell

У меня есть пользовательский UITableViewCell, который отображает различные атрибуты объекта Person (подкрепленные Core Data)... некоторые ярлыки, изображения и т.д. В настоящее время я заставляю весь tableview перезагружать всякий раз, когда изменяется какое-либо свойство, и что, очевидно, неэффективно, Я знаю, что с KVO я должен добавить слушателя в ярлык в ячейке, который может прослушивать изменения свойств Person. Но я не уверен, как его реализовать и не могу найти никаких примеров.

Вот что я обычно делаю в своем UITableView 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;
}

Ячейка подключена к IB. Я бы хотел определить, когда изменяется имя displayName, и обновить только метку имени. Благодаря

Ответ 1

Для фона вы, вероятно, захотите прочитать руководства по наблюдению за ключевыми значениями и указаниями по ключевым значениям, если вы еще этого не сделали. Затем просмотрите методы категорий NSKeyValueObserving.

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

Вкратце, вам нужно тщательно управлять добавлением и удалением объекта наблюдения в список наблюдаемых объектов наблюдателей (помиловать кажущуюся избыточность этого утверждения). Вы не хотите, чтобы объект уходил с наблюдателями, которые все еще зарегистрированы, или вы получаете жалобы и возможные другие проблемы.

Тем не менее, вы используете -addObserver:keyPath:options:context для добавления объекта в качестве наблюдателя. Контекст должен быть статически объявленной строкой. Аргумент options определяет, какие данные вы возвращаете в свой метод наблюдения (см. Ниже). KeyPath - это путь имен свойств от наблюдаемого объекта к наблюдаемому свойству (это может пересекать несколько объектов и будет обновляться при изменении промежуточных объектов, а не только при изменении свойства листа).

В вашем случае вы можете увидеть метку и использовать клавишу text keyPath или ячейку и использовать путь ключа nameLabel.text. Если класс представления таблицы был разработан по-разному, вы могли бы наблюдать весь массив ячеек, но такого свойства в UITableView не существует. Проблема с наблюдением за ячейкой заключается в том, что представление таблицы может удалить ее в любое время (если в вашем проекте используются несколько ячеек, которые выполняют одну и ту же цель в списке с переменной длиной). Если вы знаете, что ваши ячейки статичны, вы можете, вероятно, наблюдать за ними, не опасаясь.

Как только у вас зарегистрирован наблюдатель, этот наблюдатель должен реализовать -observeValueForKeyPath:ofObject:change:context:, подтвердите соответствие контекста (просто сравните значение указателя с вашим статическим строковым адресом, в противном случае вызовите супер-реализацию), затем загляните в словарь изменений для требуемых данных (или просто спросите объект для него напрямую) и используйте его, чтобы обновить свою модель по своему усмотрению.

Есть много примеров KVO в примере кода, в том числе на сайте разработчика Apple, и как часть образцов привязок на сайте Malcolm Crawford (mmalc), но большая часть из них относится к Mac OS X, а не к iOS.

Ответ 2

Вышеупомянутый ответ подходит для статических ячеек. Использование KVO для UITableViewCell все еще работает с повторным использованием ячеек. Добавьте наблюдателей, которые вам нужны, когда ячейка появится, и удалите их, когда ячейка больше не отображается. Единственный трюк в том, что Apple, похоже, не согласна с отправкой didEndDisplayingCell:, поэтому наблюдатели должны быть удалены в двух местах на 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];
    }
}

Если наблюдатели не очищаются, может случиться следующее. Наблюдатель может попытаться уведомить какой-либо объект в этой ячейке памяти, которая может даже не существовать.

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

Ответ 3

Это работает:

В configureCell:

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

В CustomCell:

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

Ответ 4

В моем случае я добавил наблюдателя на пользовательскую метку ячейки forKeyPath "text" с параметрами (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld).

При наблюдении значения для keyPath я проверяю, чтобы keyPath был тем, который я хочу, как дополнительная мера, а затем я вызываю свой метод для того, что когда-либо выполнял я, что метка

например, в моем случае

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

}

Ответ 5

Я предпочитаю решение, в котором UITableViewCell делает все KVO самостоятельно. Моя настройка такова:

В моем подклассе ячейки у меня есть свойство, которое сохраняет сильную ссылку на мой класс модели, из которого я извлекаю свои данные, и метод, который я вызываю, когда я хочу прикрепить новый объект к свойству:

@interface MyTableViewCell : UITableViewCell

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

Реализация:

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

Обратите внимание, что фактический KVO обрабатывается в фоновом потоке, чтобы избежать блокировки основного потока во время прокрутки. Также обратите внимание, что -populateFromObject: немедленно возвращается и, следовательно, отображает непосещенную ячейку. Чтобы этого избежать, мы скрываем представление содержимого, пока ячейка не будет полностью заполнена. Теперь единственное, что осталось реализовать, - это метод класса на YourModelObject, который возвращает массив ключей, которые вы хотите использовать KVO:

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

.. и в 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;
}

Сильная ссылка от ячейки к объекту модели гарантирует, что объект не будет освобожден, в то время как ячейка все еще наблюдает одно из своих свойств, то есть видно. Когда ячейка освобождается, KVO не регистрируется, и только тогда объект модели будет освобожден. Для удобства у меня также есть слабая ссылка от объекта модели обратно на ячейку, которая может пригодиться при реализации методов делегирования UITableView.