key-value observer

Не подумайте ничего дурного. Просто при слове observer я вспоминаю лучшую стратегию всех времен Star Craft. Вы за кого играли или может быть играете? Я Зерга любил. А observer был у Протоссов. Ладно, перейдем к делу.

Что такое Key-Value Observing (KVO) в Objective-C?

Если мы заглянем на страницу официальной документации Apple, то обнаружим там следующее.

Key-value observing предоставляет механизм, который позволяет одним объектам быть оповещенными об изменении отдельных свойств других объектов. Это особенно применимо для коммуникации между моделью и контроллерами в приложении. В то же время модель может наблюдать за объектами другой модели или за другими объектами самой себя.

«Короче Склифософский!» Key-Value Observing?

Разберем KVO на простом примере. Предположим перед вами стоит задача прикрутить следующую фичу к приложению. Нужно добавить кнопку в виде смайлика, по нажатию на которую будет открываться NSPopover. Смайлик при этом должен менять вид. NSPopver закрывается при нажатии в любом месте экрана. Смайлик должен вернуться в первоначальное состояние.

key-value observing

Задача, в принципе, очень простая.

При нажатии на кнопку показываем NSPopover и вызываем метод, который обновляет состояние кнопки, в котором, собственно и меняем смайлик.

- (IBAction)showFeedbackPopover:(id)sender
{
    [m_pFeedbackPopover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinXEdge];
    [self updateFeedbackButtonWithPressedState:YES];
}

- (void)updateFeedbackButtonWithPressedState:(BOOL)pressed
{
    if (pressed) {
        [m_pFeedbackButton setImage:[NSImage imageNamed:@"Smile_hover"]];
        [m_pFeedbackButton setAlternateImage:[NSImage imageNamed:@"Smile"]];
    } else {
        [m_pFeedbackButton setImage:[NSImage imageNamed:@"Smile"]];
        [m_pFeedbackButton setAlternateImage:[NSImage imageNamed:@"Smile_hover"]];
    }
}

Вопрос в том, как вернуть смайлик в первоначальное состояние, когда NSPopover закроется?

И тут нам поможет  Key-Value Observing.

Если мы заглянем в класс NSPopver, то обнаружим в нем свойство shown, которое возвращает YES, когда NSPopover показывается, и NO — когда спрятан.

/*  YES if the popover is being shown, NO otherwise. 
 */
@property(readonly, getter=isShown) BOOL shown;

Для того, чтобы нам решить нашу задачу, нам нужно как-то следить за этим свойством shown и менять смайлик, когда оно изменяется. Это и делает KVO.

Первое, что нам надо сделать, добавить обсервер. Обычно это можно сделать в методах viewDidLoad или awakeFromNib

    [m_pFeedbackPopover addObserver:self
                         forKeyPath:@"shown"
                            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                            context:nil];

Мы только что добавили обсервер к нашему экземпляру класса NSPopover, который будет следить за изменениями свойства shown. В опциях мы указали отслеживать новое значение и старое.

Теперь нам нужно реализовать метод, куда, собственно, будут приходить все эти изменения. Этот метод называется

— (void) observeValueForKeyPath: ofObject: change: context:

и является методом класса NSKeyValueObserving фреймворка Foundation.

#pragma mark - KVO Observing

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context
{
    NSLog(@"\nobserveValueForKeyPath: %@\nofObject: %@\nchange: %@", keyPath, object, change);
}

Запустим наше приложение и нажмем на смайлик 1 раз для открытия диалога. Теперь нажмем в любом месте экрана для закрытия диалога. NSLog нам вывел следующее сообщение.

2016-03-22 16:42:40.589 Annotator[52163:1197836] 
observeValueForKeyPath: shown
ofObject: <NSPopover: 0x600000123c00>
change: {
    kind = 1;
    new = 1;
    old = 0;
}
2016-03-22 16:42:42.975 Annotator[52163:1197836] 
observeValueForKeyPath: shown
ofObject: <NSPopover: 0x600000123c00>
change: {
    kind = 1;
    new = 0;
    old = 1;
}

Как мы можем видеть нам пришел NSDictionary *change с ключами kind, new и old. И как мы можем видеть в первом случае новое значение shown стало 1, старое 0. Когда мы закрыли окно — shown изменился в первоначальное состояние.

Вот и все. Нам остается преобразовать значение ключей в BOOL (т.к. ключи содержат NSString* как можно видеть в описании метода) и вызвать метод, обновляющий состояние нашей кнопки.

#pragma mark - KVO Observing

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context
{
    NSString *kChangeNew = [change valueForKey:@"new"];
    BOOL shown = kChangeNew.boolValue;
    [self updateFeedbackButtonWithPressedState:shown];
}

В этом и есть Key-Value Observing.