我有一个自定义的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更改的时间,并只更新名称标签。 感谢
答案 0 :(得分:18)
以上答案非常适合静态细胞。对UITableViewCell
使用KVO仍可用于单元重用。在单元格即将出现时添加所需的观察者,并在不再显示单元格时将其删除。唯一的诀窍是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>)
答案 1 :(得分:5)
对于背景,您可能希望阅读键值观察和键值编码指南(如果尚未阅读)。然后查看NSKeyValueObserving类别方法。
简而言之,您需要仔细管理向观察对象的观察对象列表添加和删除观察对象(原谅该语句的看似冗余)。你不希望观察者仍然注册一个对象,或者你得到投诉和可能的其他问题。
也就是说,您使用-addObserver:keyPath:options:context
添加对象作为观察者。 Context应该是静态声明的字符串。 options参数控制您在观察方法中获得的数据(见下文)。 keyPath是从观察对象到被观察属性的属性名称的路径(这可以遍历多个对象,并且将在中间对象更改时更新,而不仅仅在叶属性更改时更新。)
在您的情况下,您可以观察标签,并使用text
keyPath或单元格,并使用nameLabel.text
密钥路径。如果表视图类的设计不同,您可能会观察整个单元格数组,但UITableView上没有这样的属性。观察单元格的问题是表视图可能随时删除它(如果您的设计在可变长度列表中使用多个单元用于相同的目的)。如果你知道你的细胞是静止的,你可以毫无顾虑地观察它们。
一旦您注册了观察者,该观察者必须实施
-observeValueForKeyPath:ofObject:change:context:
,确认上下文匹配(只需将指针值与静态字符串的地址进行比较;否则,调用super的实现),然后查看更改字典中的所需数据(或直接向对象询问它)并根据您的需要使用它来更新您的模型。
示例代码中有很多KVO示例,包括Apple的开发人员网站,以及作为Malcolm Crawford(mmalc)网站上的绑定示例的一部分,但大多数是针对Mac OS X,而不是iOS。 / p>
答案 2 :(得分: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"];
}
答案 3 :(得分:1)
在我的情况下,我使用选项(NSKeyValueObservingOptionNew
| NSKeyValueObservingOptionOld
)向自定义单元格标签forKeyPath“text”添加了一个观察者。
当观察 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'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];
}
答案 4 :(得分:0)
我更喜欢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 [[self.object class] 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
委托方法时可以派上用场。