mick@next:~/objc/kvo-deep-dive$/* Rediscovering the language that built the future */
Key-Value Observing Deep Dive
objc / kvo-deep-dive.m

Key-Value Observing Deep Dive

Mick MacCallumMick MacCallum
@Objective-C@Foundation@Advanced

Key-Value Observing is one of Objective-C's most powerful—and most misused—features. It allows any object to watch for changes to another object's properties, receiving callbacks automatically. But its elegance comes with sharp edges. Let's explore how it works and how to wield it safely.

The Basic Pattern

Register as an observer, implement the callback, remove yourself before deallocation:

// Start observing
[person addObserver:self
         forKeyPath:@"name"
            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
            context:nil];

// Receive changes
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSString *oldName = change[NSKeyValueChangeOldKey];
        NSString *newName = change[NSKeyValueChangeNewKey];
        NSLog(@"Name changed from %@ to %@", oldName, newName);
    }
}

// Stop observing (required!)
- (void)dealloc {
    [person removeObserver:self forKeyPath:@"name"];
}

How KVO Works Under the Hood

When you add an observer for the first time, the runtime performs magic: it dynamically creates a subclass of the observed object's class. This subclass, with a name like NSKVONotifying_Person, overrides the observed property's setter to call the notification machinery.

// Before observation
[person class];  // Person

[person addObserver:self forKeyPath:@"name" ...];

// After observation
[person class];           // Still returns Person (lies!)
object_getClass(person);  // NSKVONotifying_Person (the truth)

Apple hides this by overriding -class to return the original class. The object_getClass runtime function reveals the truth.

Automatic vs Manual Notifications

By default, KVO triggers automatically when you set a property through its setter:

person.name = @"Alice";  // Triggers notification
person->_name = @"Bob";  // Doesn't trigger - bypasses setter

For complex cases, you can disable automatic notifications and trigger them manually:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

This lets you control exactly when notifications fire, batch multiple changes, or notify for computed properties.

Observation Options

The options parameter controls what information the change dictionary contains:

NSKeyValueObservingOptionNew   // Include new value in change dict
NSKeyValueObservingOptionOld   // Include old value
NSKeyValueObservingOptionInitial  // Fire immediately with current value
NSKeyValueObservingOptionPrior    // Notify before AND after change

Combining them:

[object addObserver:self
         forKeyPath:@"progress"
            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial
            context:nil];

The Context Parameter

Multiple observations of the same key path can collide. Use context to disambiguate:

static void *PersonNameContext = &PersonNameContext;
static void *TeamNameContext = &TeamNameContext;

[person addObserver:self forKeyPath:@"name" options:0 context:PersonNameContext];
[team addObserver:self forKeyPath:@"name" options:0 context:TeamNameContext];

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if (context == PersonNameContext) {
        // Handle person name change
    } else if (context == TeamNameContext) {
        // Handle team name change
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

Always call super for unrecognized contexts—a superclass might be observing.

Dependent Keys

Sometimes a property depends on other properties:

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, readonly) NSString *fullName;
@end

@implementation Person

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}

@end

Now observing fullName automatically triggers when firstName or lastName changes.

Collection Observation

Observing an array property requires special handling. Simply modifying the array doesn't trigger KVO:

// Doesn't trigger KVO
[person.friends addObject:newFriend];

// Does trigger KVO
[[person mutableArrayValueForKey:@"friends"] addObject:newFriend];

The mutableArrayValueForKey: method returns a proxy that properly triggers notifications.

Common Crashes

Forgetting to remove observers:

// CRASH: Observer deallocated while still registered
- (void)dealloc {
    // Oops, forgot to remove observer
}

The observed object tries to notify a dead observer. Crash.

Removing non-existent observers:

// CRASH: Removing observer that was never added (or already removed)
[person removeObserver:self forKeyPath:@"name"];
[person removeObserver:self forKeyPath:@"name"];  // Crash on second call

Track your observation state carefully.

Thread safety:

Notifications are delivered on whatever thread the change occurs. If you're updating UI:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateUI];
    });
}

Modern Alternatives

Swift's property observers and Combine often provide better alternatives:

// Swift property observer - simpler for self-observation
var name: String {
    didSet { print("Name changed to \(name)") }
}

// Combine - reactive streams
person.$name
    .sink { name in print("Name is now \(name)") }
    .store(in: &cancellables)

But KVO remains essential for observing properties on objects you don't control—system classes, third-party code, or UIKit internals.

Best Practices

Use a dedicated context pointer for each observation. Remove observers in dealloc or when you're done observing. Be cautious with collection properties—use the mutable proxy methods. Consider whether a delegate or notification pattern might be simpler.

KVO is powerful and unique to the Objective-C world. Master it, respect its quirks, and it will serve you well.