Добавление 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
.