Key-Value Observing Deep Dive
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.