Key-Value Coding and Observing
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Key-value coding (KVC) is indirect access to an object’s properties by name, at run time. Key-value
observing (KVO) builds on it to notify interested parties when a property changes. Together they are the
foundation of Cocoa bindings, Core Data, NSPredicate, NSSortDescriptor and a great deal of framework glue — and they are entirely dependent on the accessor naming conventions described in
Properties and Encapsulation.
Key-Value Coding
Reading and Writing by Name
Person *p = [[Person alloc] initWithName:@"Ada" age:36];
// Instead of p.name / p.age:
NSString *name = [p valueForKey:@"name"];
NSNumber *age = [p valueForKey:@"age"]; // scalars are boxed automatically
[p setValue:@"Grace" forKey:@"name"];
[p setValue:@45 forKey:@"age"]; // unboxed automatically
Scalars are boxed into NSNumber (or NSValue for structs) on the way out and unboxed on the way in, which
is what lets generic code treat every property uniformly.
How a Key Is Resolved
valueForKey:@"name" searches, in order:
-
-getName,-name,-isName,-_name -
The to-many accessors (
-countOfNameplus-objectInNameAtIndex:or-enumeratorOfName) -
If
+accessInstanceVariablesDirectlyreturnsYES(the default), the ivars_name,_isName,name,isName -
-valueForUndefinedKey:, whose default implementation raisesNSUnknownKeyException
setValue:forKey: mirrors this with -setName:/-_setName:, then the ivars, then
-setValue:forUndefinedKey:.
A class following the ordinary @property conventions is therefore KVC-compliant automatically. Overriding
the undefined-key methods is how you make a class tolerant of unknown keys — useful when mapping JSON:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"ignoring unknown key %@", key); // instead of raising
}
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
Note that KVC reaching an ivar directly, with no accessor, is a real encapsulation hole. Override
+accessInstanceVariablesDirectly to return NO if you want that closed.
Key Paths
A key path is a dotted chain traversed one link at a time:
NSString *city = [person valueForKeyPath:@"address.city"];
[person setValue:@"Dublin" forKeyPath:@"address.city"];
NSString *street = [order valueForKeyPath:@"customer.address.street"];
If any link is nil, the whole expression returns nil — key paths short-circuit rather than crash.
Write key paths with the compile-time-checked @keypath-style idiom where you can. Clang offers
#keyPath in Swift; in Objective-C the closest equivalents are NSStringFromSelector(@selector(name)),
which at least breaks when the method is renamed:
[object addObserver:self
forKeyPath:NSStringFromSelector(@selector(progress)) // better than @"progress"
options:NSKeyValueObservingOptionNew
context:MyContext];
Collection Operators
Applied to a collection, a key path may include an @ operator:
NSArray<Person *> *people = …;
[people valueForKeyPath:@"@count"]; // number of elements
[people valueForKeyPath:@"@sum.age"]; // total
[people valueForKeyPath:@"@avg.age"]; // mean
[people valueForKeyPath:@"@max.age"]; // maximum
[people valueForKeyPath:@"@min.age"];
// Array operators
[people valueForKeyPath:@"@unionOfObjects.name"]; // every name, duplicates kept
[people valueForKeyPath:@"@distinctUnionOfObjects.city"]; // unique cities
[nested valueForKeyPath:@"@unionOfArrays.items"]; // flatten one level
valueForKey: applied to an array is itself a map: [people valueForKey:@"name"] returns the array of names.
That is the nearest thing Foundation offers to a built-in map.
The aggregate operators use NSNumber arithmetic and ignore nil, which makes them convenient for quick
summaries but unsuitable for precise financial arithmetic.
Validation
KVC defines a validation hook per key, which bindings and Core Data call automatically:
- (BOOL)validateAge:(id *)ioValue error:(NSError **)outError {
NSNumber *age = *ioValue;
if (age.integerValue < 0) {
if (outError) {
*outError = [NSError errorWithDomain:MyErrorDomain
code:MyErrorCodeInvalidAge
userInfo:@{NSLocalizedDescriptionKey: @"Age cannot be negative"}];
}
return NO;
}
if (age.integerValue > 150) {
*ioValue = @150; // coerce rather than reject -- note the by-reference parameter
}
return YES;
}
NSError *error = nil;
id value = @(-5);
if ([person validateValue:&value forKey:@"age" error:&error]) {
[person setValue:value forKey:@"age"];
}
Validation is not automatic: setValue:forKey: does not call it. The caller must, which is why frameworks
that manage editing (bindings, Core Data) do it on your behalf and hand-written code usually must too.
Key-Value Observing
KVO notifies an observer whenever an observed property changes. Its remarkable property is that the observed class needs no cooperation at all — any KVC-compliant property is observable.
Registering and Responding
// A unique context: the address of a static, so it cannot collide with anyone else's.
static void * const MyProgressContext = (void *)&MyProgressContext;
- (void)startObserving {
[self.download addObserver:self
forKeyPath:NSStringFromSelector(@selector(progress))
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:MyProgressContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if (context == MyProgressContext) {
NSNumber *newValue = change[NSKeyValueChangeNewKey];
NSNumber *oldValue = change[NSKeyValueChangeOldKey];
[self updateProgressBar:newValue.doubleValue];
} else {
// NOT ours -- a superclass may be observing something. Always pass it on.
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)stopObserving {
[self.download removeObserver:self
forKeyPath:NSStringFromSelector(@selector(progress))
context:MyProgressContext];
}
The context pointer is not optional ceremony. Without it you cannot distinguish your own registrations from a
superclass’s, and both the if above and the [super …] fallthrough exist precisely to keep those separate.
| Option | Effect |
|---|---|
|
|
|
|
|
Fire once immediately on registration — handy for initial UI sync. |
|
Fire before the change as well as after. |
The Notification Sequence
(ViewController) participant D as Observed object
(Download) participant R as Objective-C runtime O->>D: addObserver:forKeyPath:@"progress"
options:New context:MyProgressContext D->>R: first observer for this class R->>R: create NSKVONotifying_Download at run time,
override -setProgress: and -class,
and swizzle the instance's isa to it Note over R: The object's class silently changes.
[obj class] still reports "Download". O->>D: (later) someone calls setProgress: 0.5 D->>R: the KVO subclass's setter runs R->>R: willChangeValueForKey:@"progress" R->>D: call the ORIGINAL setProgress: R->>R: didChangeValueForKey:@"progress" R-->>O: observeValueForKeyPath:ofObject:change:context: O->>O: check context == MyProgressContext O->>O: read change[NSKeyValueChangeNewKey]
and update the progress bar O->>D: removeObserver:forKeyPath:context: Note over O,D: MANDATORY before EITHER object is deallocated.
Observed object first: "was deallocated while key value
observers were still registered with it."
Observer first: silent crash on the next notification.
The mechanism is worth knowing: on first registration the runtime creates a hidden subclass
(NSKVONotifying_Download), overrides the setters of the observed keys and the class method (so the
substitution stays invisible), and points the instance’s isa at it. This is why KVO works on classes that
know nothing about it — and why swizzling an observed class’s setters interacts badly with KVO.
Automatic and Manual Notification
Notification is automatic for any property changed through its setter — which is the main practical reason to
use self.property = x rather than _property = x outside initialisers.
When you must change state without a setter, bracket it manually:
- (void)recomputeInternally {
[self willChangeValueForKey:@"progress"];
_progress = [self computeProgress]; // direct ivar write
[self didChangeValueForKey:@"progress"];
}
To suppress automatic notification for a key (because you send it yourself):
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"progress"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
Dependent Keys
A computed property must declare what it depends on, or observers of it will never fire:
// Option 1 -- the generic hook.
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[ @"firstName", @"lastName" ]];
}
return keyPaths;
}
// Option 2 -- the per-key convenience, named +keyPathsForValuesAffecting<Key>.
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
Now observing fullName fires whenever either component changes.
Removing Observers
This is KVO’s sharpest edge, and there are two distinct failures — the fix for one is not the fix for the other.
The observed object dies first. Deallocating an object that still has registrations against it raises:
An instance 0x600000abc123 of class Download was deallocated while key value observers were still registered with it.
The observer dies first. No exception is raised; the next change notification is instead delivered to freed memory, and the process crashes later inside the KVO machinery with no diagnostic naming the cause. This is the harder one to debug precisely because it is silent until it isn’t.
Both come down to the same missing removeObserver:forKeyPath:context: — but note that removing observers
in the observer’s dealloc only prevents the second failure. If the observed object can outlive nothing
and be deallocated first, its own teardown has to unregister too.
There is no weak-observer mechanism and no automatic cleanup. The rules:
-
Remove in
dealloc, or earlier at a well-defined point (viewWillDisappear:, astopmethod). -
Removing an observer that was never added also throws, so track registration state if the paths are not perfectly symmetric.
-
Always pass the same
contexttoremoveObserver:forKeyPath:context:that you passed when adding.
- (void)dealloc {
if (_isObserving) {
[_download removeObserver:self
forKeyPath:NSStringFromSelector(@selector(progress))
context:MyProgressContext];
}
}
Because this is so error-prone, many codebases wrap KVO in a small observer object whose own dealloc does
the removal, or avoid it in favour of delegates, blocks or notifications.
NSNotificationCenter: the Broadcast Alternative
KVO watches one property on one object. NSNotificationCenter broadcasts named events to any number of
listeners, with no coupling in either direction:
// Posting
NSString * const MyDownloadDidFinishNotification = @"MyDownloadDidFinishNotification";
[[NSNotificationCenter defaultCenter] postNotificationName:MyDownloadDidFinishNotification
object:self
userInfo:@{ @"bytes": @(total) }];
// Observing, selector-based
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(downloadDidFinish:)
name:MyDownloadDidFinishNotification
object:nil]; // nil = from any sender
- (void)downloadDidFinish:(NSNotification *)note {
NSNumber *bytes = note.userInfo[@"bytes"];
id sender = note.object;
}
// Observing, block-based -- returns a token you must keep AND remove
id token = [[NSNotificationCenter defaultCenter]
addObserverForName:MyDownloadDidFinishNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) { … }];
[[NSNotificationCenter defaultCenter] removeObserver:token];
Notifications are delivered synchronously, on the posting thread — if you post from a background queue,
your handler runs there too, which is a common source of UI-from-background bugs. The block-based API’s
queue: parameter is the clean way to avoid that.
Since macOS 10.11 / iOS 9 the selector-based observer is unregistered automatically on deallocation, but block-based tokens are not: keep and remove them explicitly.
Choosing
| Mechanism | Use when |
|---|---|
Delegate |
One-to-one, several related callbacks, and the sender needs answers back. |
Block |
A single "here is the result" callback. |
KVO |
You need to watch a property’s value change on a specific object, especially one whose class you do not control. |
Notification |
One-to-many broadcast, or the sender and receiver should not know about each other at all. |
See Protocols and Delegation and Blocks.
See Also
-
Properties and Encapsulation — the accessor naming KVC depends on.
-
The Objective-C Runtime — the runtime subclassing behind KVO.
-
Collections and Fast Enumeration —
NSPredicateandNSSortDescriptor, which are KVC-based. -
Concurrency — which thread a notification arrives on.