Правильный KVO

| Комментарии

Для реализации KVO, объект должен реализовать неформальный протокол NSKeyValueObserving. NSObject уже предоставляет реализацию этого протокола, поэтому разработчику нужно лишь грамотно использовать готовый механизм.

Вроде как в KVO нет ничего сложного.

Чтобы подписаться надо вызвать:

1
- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context

Чтобы отписаться:

1
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context

Ну и остается только правильно реализовать метод в который приходят уведомления:

1
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

Большинство разработчиков умеют правильно подписываться/отписываться, но с observeValueForKeyPath еще возникают проблемы. Вот примеры неправильной реалзиции этого метода из реальных проектов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  if (context != (__bridge void *)kActionLoadingStateObservationContext)
    return;
  NSUInteger index = [self.actions indexOfObject:object];
  NSAssert(index != NSNotFound, @"Can not found action %@ in the current actions", object);
  // update loading state  
  YMTAction *action = self.actions[index];
  YMTActionView *actionView = self.subviews[index];
  actionView.loading = action.loading;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  if (object == self.webView.scrollView && [keyPath isEqualToString:@"contentSize"]) {
    self.webViewHeightConstraint.constant = [change[NSKeyValueChangeNewKey] CGSizeValue].height;
  }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  if ([keyPath isEqual:@keypath(YRCachedSettings.new, selectedSuburbanZone)]) {
    [self zoneDidChange];
  }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  [self reloadData];
}

Главная проблема у всех показанных реализаций, это отсутствие вызова [super observeValueForKeyPath:keyPath:change:context:]. Многие забывают о том, что базовый класс тоже может использовать KVO. Порой даже не забывают, а сознательно опускают вызов метода, так как, когда наткнулись на то, что базовы класс кинул NSInternalInconsistencyException.

Собсвтенно, чтобы не было проблем с базовым классом, [super observeValueForKeyPath:keyPath:change:context:] надо вызвать не при каждом уведомление, а только при тех, на которые вы не подписывались. Чтобы отличить свои уведомления от чужих (принадлежащих базовому классу) надо использовать контекст.

Вот пример правильного observeValueForKeyPath:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(__unused NSDictionary *)change context:(void *)context {
   if (context == &ja_kvoContext) {
     if ([keyPath isEqualToString:@"view"]) {
       if (self.centerPanel.isViewLoaded && self.recognizesPanGesture) {
         [self _addPanGestureToView:self.centerPanel.view];
       }
     } else if ([keyPath isEqualToString:@"viewControllers"] && object == self.centerPanel) {
       // view controllers have changed, need to replace the button  
       [self _placeButtonForLeftPanel];
     }
   } else {
     [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
   }
 }

Комментарии