KVO (Key-Value Observing) looks simple to use. Register with addObserver, and you receive a notification whenever the property changes.
The related article Objective-C Method Swizzling Explained covers the background concepts and related application examples.
But isn’t that strange? Without adding notification code to the property setter, how does the runtime know that the value changed?
The answer is bold. The moment you attach an observer, the runtime secretly swaps the object’s class.
This technique is called isa-swizzling. If the method swizzling from the previous article changed a “line” in a dispatch table, this time we replace the “class” the object belongs to.
What happens when addObserver is called
Person When you attach an observer to an object, the runtime performs the following steps.
- It dynamically creates
NSKVONotifying_Person, a subclass. - It overrides the setter of the observed property with a version that inserts notification code before and after the change.
- It replaces the object’s isa pointer with the new subclass.
After that, when person.name = @"Kim" executes, the call flow looks like this.
// NSKVONotifying_Person's pseudocode overriding setter
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name]; // execute the original setter
[self didChangeValueForKey:@"name"]; // notify the observer here
}
The object’s memory remains unchanged; only its class changes, so existing code notices nothing. When the last observer is removed, isa returns to the original class.
The class lies: class vs object_getClass
Here comes an interesting detail. Ask an observed object for its class, and you can get two different answers.
[person class]; // Person — lie
object_getClass(person); // NSKVONotifying_Person — truth
The runtime also overrides -class on the dynamic subclass so that it is made to answer the original class. This hides implementation details and prevents developers from wondering, “Why does my object’s class have such a strange name?”
object_getClass() reads the isa pointer directly, so it reveals the real class. If the two values differ during debugging, the object is currently being observed by someone.
No notification without going through the setter
Once you understand isa-swizzling, KVO’s famous limitation becomes obvious.
person.name = @"Kim"; // notification occurs — via the overridden setter
person->_name = @"Kim"; // no notification — ivar direct modification, setternot traversed
The source of KVO notifications is the overridden setter. Directly modifying the ivar bypasses that path, so nothing happens. This is one practical reason Objective-C recommends dot syntax for property access, which internally calls the setter.
If you must change a value without using the setter, wrap the change in manual notifications.
[self willChangeValueForKey:@"name"];
_name = @"Kim";
[self didChangeValueForKey:@"name"];
Conversely, to disable automatic notifications for a property, return NO from automaticallyNotifiesObserversForKey:.
Things to watch for in production
Managing the observation lifetime remains important. You must retain the NSKeyValueObservation token returned by the block-based API while observation is needed (Apple KVO documentation).
When observation ends, invalidating or releasing the token also removes the registration. If you keep using the string-based API, you must coordinate the lifetimes of addObserver and removeObserver yourself (Apple KVO documentation).
Mixing KVO and method swizzling in the same class creates ordering problems. If KVO is added after swizzling, the dynamic subclass wraps the swizzled setter; in the opposite order, their assumptions can conflict.
When two runtime-level techniques meet on one object, debugging difficulty multiplies.
In Swift, @objc dynamic is required.
class Person: NSObject {
@objc dynamic var name: String = ""
}
let observation = person.observe(\.name, options: [.new]) { _, change in
print(change.newValue ?? "")
}
isa-swizzling works only when setter calls travel through the Objective-C message path.
Since Swift 4, use the block-based observe(_:options:changeHandler:) API. When the returned NSKeyValueObservation token is released, it automatically unregisters the observation. Using Key-Value Observing in Swift
This structure prevents most of the problems associated with the old string-based keyPath approach.
Summary
- When an observer is registered, KVO dynamically creates the NSKVONotifying_ subclass and swaps isa
- The source of notifications is the overridden setter—a version with willChange/didChange inserted
-classis made to answer the original class, while the real class appears asobject_getClass()- Direct ivar modification bypasses the setter, so no notification is sent—use willChange/didChange for manual notification when needed
- In Swift,
@objc dynamicplus the block-based observe API is standard, and releasing the token unregisters the observation
Once you include method swizzling and isa-swizzling, the Objective-C runtime comes into focus. Both method dispatch tables and the classes objects belong to are runtime-replaceable data—this flexibility powers much of Cocoa’s magic.
Continue reading
- Objective-C Method Swizzling Explained
- +load vs +initialize: Call Timing and Inheritance Traps
- Objective-C Categories vs Swift Extensions: Conceptual Differences and Key Caveats
Sources and verification
- Using Key-Value Observing in SwiftApple · Official documentation · Checked August 17, 2026Supports: Registering Swift KVO, change notifications, and observation-token lifetimes

