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, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

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:

  1. -getName, -name, -isName, -_name

  2. The to-many accessors (-countOfName plus -objectInNameAtIndex: or -enumeratorOfName)

  3. If +accessInstanceVariablesDirectly returns YES (the default), the ivars _name, _isName, name, isName

  4. -valueForUndefinedKey:, whose default implementation raises NSUnknownKeyException

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

NSKeyValueObservingOptionNew

change[NSKeyValueChangeNewKey] holds the new value.

NSKeyValueObservingOptionOld

change[NSKeyValueChangeOldKey] holds the previous value.

NSKeyValueObservingOptionInitial

Fire once immediately on registration — handy for initial UI sync.

NSKeyValueObservingOptionPrior

Fire before the change as well as after.

The Notification Sequence

sequenceDiagram participant O as Observer
(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:, a stop method).

  • Removing an observer that was never added also throws, so track registration state if the paths are not perfectly symmetric.

  • Always pass the same context to removeObserver: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 Also