Getting to grips with KVO

What was going to be a follow-up post got delayed, so now it’s a full post of its very own.

The problem

Here is the current setup for my app’s inspector panel.

In a separate window I have a graph, with the selected series “bolded” (line thickness increased). When I change the selection of the dropdown box in this panel:

Here is how the bindings were originally setup.

This is pretty standard. But there’s a problem: I have no way of getting the graph to redraw when the selection changes.

First solution

My first solution was to set up the bindings as follows.

Then, in JRWindowController, I made methods like the following:

1
2
3
4
5
6
7
8
-(BOOL)visible {
    return [selectedSeries visible];
}

-(void)setVisible:(BOOL)visible {
    [selectedSeries setVisible:visible];
    [graph setNeedsDisplay:YES];
}

There’s a couple of problems with this code, namely:

  1. Duplication: If I end up with five different methods that all need to refresh the graph, I’m creating a bunch of code that does pretty much the same thing five times.
  2. It doesn’t work: While the graph will redraw, the controls in the panel will not. I believe this is because we’re hooking up the KVO to the WindowController’s methods. For whatever particular reason, the controls on the panel won’t update to reflect our selection.

Second solution

It seemed this was a perfect time to learn about KVO and how to do it manually, rather than spending this whole time using Interface Builder’s GUI to do the heavy lifting. Most of it ended up being done by the following method call:

1
2
3
4
[selectedSeries addObserver:self
    forKeyPath:@"visible"
    options:NSKeyValueObservingOptionNew
    context:NULL];

Then, later in the class definition:

1
2
3
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    [graphView setNeedsDisplay:YES];
}

Since there were several values I wanted to observe, I actually bundled the whole thing up in a for loop to iterate over them:

1
2
3
4
5
6
for (NSString *key in observedKeys)
    [selectedSeries addObserver:self
        forKeyPath:key
        options:NSKeyValueObservingOptionNew
        context:NULL
    ];

Finally, I just needed to add some code to start and stop observing series, depending on whether or not they were selected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-(void)startObservingSeries {
    for (NSString *key in observedKeys)
        [selectedSeries addObserver:self
            forKeyPath:key
            options:NSKeyValueObservingOptionNew
            context:NULL];
}

-(void)stopObservingSeries {
    for (NSString *key in observedKeys)
        [selectedSeries removeObserver:self forKeyPath:key];
}

-(void)setSelectedSeries:(JRGraphSeries *)newSelectedSeries {
    if (selectedSeries != NULL)
        [self stopObservingSeries];

    selectedSeries = newSelectedSeries;
    [self startObservingSeries];

    [graphView setNeedsDisplay:YES];    
}

Now KVO works correctly (a change to selectedSeries triggers the right KVO methods to update panel controllers), the graph redraw works correctly (my own KVO code makes sure that happens), and even better, I’ve removed some code duplication. And, as a final cherry on the top, I understand not only how KVO works, but how to manually implement it in my own projects.