Swift & Objective-C

KVO Internals Explained: The Truth Behind isa-Swizzling

The moment you attach an observer, KVO dynamically swaps the object's class with a subclass. This covers isa-swizzling, why class lies, and when notifications are not sent without going through the setter.

4 min read
Cover image for KVO Internals Explained: The Truth Behind isa-Swizzling

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.

  1. It dynamically creates NSKVONotifying_Person, a subclass.
  2. It overrides the setter of the observed property with a version that inserts notification code before and after the change.
  3. 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.

Flow in which KVO creates the dynamic NSKVONotifying_Person subclass and replaces the Person object's isa
The order in which KVO creates a dynamic subclass and replaces isa

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.

Comparison of the Person object before KVO isa-swizzling and the NSKVONotifying_Person dynamic subclass
It looks like Person externally, but its runtime class is NSKVONotifying_Person

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 as object_getClass()
  • Direct ivar modification bypasses the setter, so no notification is sent—use willChange/didChange for manual notification when needed
  • In Swift, @objc dynamic plus 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

Sources and verification

  • Using Key-Value Observing in SwiftApple · Official documentation · Checked August 17, 2026Supports: Registering Swift KVO, change notifications, and observation-token lifetimes