Swift KVC, KVO, and dynamic: Interoperability Guide
Swift can interoperate with Objective-C-style runtime features such as Key-Value Coding, Key-Value Observing, and dynamic dispatch. This article explains what those features do, how they connect to @objc and dynamic, and when you can rely on them.
Quick answer: KVC lets you read and write properties by name at runtime, KVO lets one object observe another object’s property changes, and dynamic asks Swift to use runtime dispatch instead of static dispatch. In Swift, these features usually require Objective-C interop, which means @objc and often NSObject.
Difficulty: Intermediate
You'll understand this better if you know: basic Swift classes and properties, what NSObject is, and the difference between compile-time checks and runtime behavior.
1. What Is KVC, KVO, and dynamic?
These three terms describe related but different parts of Swift interoperability with the Objective-C runtime:
- KVC stands for Key-Value Coding. It accesses properties by string key instead of direct Swift property access.
- KVO stands for Key-Value Observing. It lets an observer receive notifications when a property changes.
- dynamic changes how method and property dispatch happens so runtime lookup can occur instead of purely static dispatch.
In Swift, these features are not part of the core language model for value types and plain structs. They are mainly used with classes that participate in Objective-C interop, usually by inheriting from NSObject and exposing members with @objc.
2. Why KVC, KVO, and dynamic Matter
These features matter when you need runtime-based behavior that cannot be expressed through normal Swift property access alone. Common reasons include observing state changes, integrating with Cocoa APIs, or working with older code that expects Objective-C runtime features.
They are especially useful when a framework or API expects property names as strings or needs automatic notification when a property changes. That said, they are less common in modern pure-Swift code because Swift’s type system, property wrappers, closures, and Combine-style patterns often provide safer alternatives.
3. Basic Syntax or Core Idea
KVC with value(forKey:)
KVC reads a property using its string name. The object must support Objective-C runtime lookup, and the key must match a property that is visible to that runtime.
import Foundation
class Person: NSObject {
@objc dynamic var name: String
init(name: String) {
self.name = name
}
}
let person = Person(name: "Ava")
let value = person.value(forKey: "name")This returns an object representation of the property value. For Swift value types, you typically need to cast the result to the expected type.
KVO with addObserver
KVO observes a property and notifies the observer when the value changes. In modern Swift, the safer API uses NSKeyValueObservation tokens.
import Foundation
class Person: NSObject {
@objc dynamic var age: Int = 20
}
let person = Person()
let observation = person.observe(\.age, options: [.old, .new]) { object, change in
if let newValue = change.newValue {
print("New age: \(newValue)")
}
}
person.age = 21The observation object must be kept alive for the observation to continue working.
dynamic for runtime dispatch
dynamic tells Swift that the member should use runtime dispatch. In practice, it is commonly combined with @objc so the member can participate in Objective-C-style mechanisms such as KVO.
class Counter: NSObject {
@objc dynamic var count: Int = 0
}
let counter = Counter()
counter.count = 1Without dynamic, some runtime features may not see the property changes the way you expect.
4. Step-by-Step Examples
Example 1: Reading a property with KVC
Here is a simple KVC read. Notice that the property must be visible to the Objective-C runtime.
import Foundation
class Book: NSObject {
@objc dynamic var title: String = "Swift Guide"
}
let book = Book()
if let title = book.value(forKey: "title") as? String {
print(title)
}This example demonstrates string-based lookup. It is flexible, but it also removes compile-time safety if the key is misspelled.
Example 2: Observing changes with modern KVO
KVO is often used to react to property updates. The block receives the object and the change details.
import Foundation
final class DownloadState: NSObject {
@objc dynamic var progress: Double = 0.0
}
let state = DownloadState()
let token = state.observe(\.progress, options: [.initial, .new]) { state, change in
if let progress = change.newValue {
print("Progress: \(progress)")
}
}
state.progress = 0.5
state.progress = 1.0
// Keep `token` alive for as long as you need observation.The key point is that the observation token controls the lifetime of the observer.
Example 3: Using KVC to set a property
KVC can also write a value by key, which is useful in bridging scenarios. The value must be the right type for the property.
import Foundation
class Profile: NSObject {
@objc dynamic var nickname: String = "guest"
}
let profile = Profile()
profile.setValue("swiftfan", forKey: "nickname")
print(profile.nickname)This is convenient when the property name is not known until runtime, but it is also easier to break accidentally than direct property assignment.
Example 4: Why a plain Swift property is not enough
When a property is not exposed to the Objective-C runtime, KVC and KVO cannot use it in the normal way. This is one of the main boundaries to remember.
class PlainModel {
var score: Int = 10
}
let model = PlainModel()
// `value(forKey:)` is not available in the same way here because this type
// does not participate in Objective-C KVC/KVO by default.For pure Swift models, prefer direct access or Swift-native observation patterns instead of forcing runtime-based APIs.
5. Practical Use Cases
- Observing UI-related model changes in Cocoa or Cocoa Touch code that still depends on KVO.
- Bridging with older Objective-C frameworks that expect KVC-compliant objects.
- Working with configuration objects that are populated from dictionaries or runtime metadata.
- Integrating with platform APIs that document KVO support explicitly.
- Using runtime lookup in tooling, editors, or serialization layers that operate by property name.
6. Common Mistakes
Mistake 1: Using KVO on a property that is not observable
KVO does not work on every Swift property. The type usually needs to be an NSObject subclass, and the property must be exposed to Objective-C runtime dispatch.
Problem: Swift may report that the property cannot be observed with KVO, or the observer simply never receives updates.
class Task {
var isDone: Bool = false
}
let task = Task()
let token = task.observe(\.isDone, options: [.new]) { _, _ in
print("Changed")
}Fix: Use an NSObject subclass and mark the property with @objc dynamic when KVO is required.
import Foundation
final class Task: NSObject {
@objc dynamic var isDone: Bool = false
}
let task = Task()
let token = task.observe(\.isDone, options: [.new]) { _, change in
print(change.newValue ?? false)
}The corrected version works because KVO can only hook into properties that participate in Objective-C runtime dispatch.
Mistake 2: Forgetting dynamic when the runtime needs it
Developers sometimes add @objc and assume that is enough. For KVO-style runtime observation, the property is commonly also marked dynamic.
Problem: The property may compile, but observation can behave unexpectedly because the runtime does not use the expected dispatch path.
import Foundation
final class Player: NSObject {
@objc var level: Int = 1
}
let player = Player()
let token = player.observe(\.level, options: [.new]) { _, change in
print(change.newValue ?? 0)
}Fix: Mark the property as dynamic when you need runtime dispatch for observation or similar mechanisms.
import Foundation
final class Player: NSObject {
@objc dynamic var level: Int = 1
}
let player = Player()
let token = player.observe(\.level, options: [.new]) { _, change in
print(change.newValue ?? 0)
}The corrected version makes the property eligible for runtime-based observation.
Mistake 3: Losing the observation token
The modern KVO API returns an observation object. If you do not store it, the observation ends early because the token is deallocated.
Problem: The code compiles, but the callback stops firing immediately or after the temporary value goes out of scope.
import Foundation
final class Download: NSObject {
@objc dynamic var status: String = "idle"
}
let download = Download()
_ = download.observe(\.status, options: [.new]) { _, change in
print(change.newValue ?? "")
}Fix: Store the token in a property or another long-lived variable.
import Foundation
final class Download: NSObject {
@objc dynamic var status: String = "idle"
}
final class Controller {
private var statusObservation: NSKeyValueObservation?
func startObserving(download: Download) {
statusObservation = download.observe(\.status, options: [.new]) { _, change in
print(change.newValue ?? "")
}
}
}The corrected version keeps the observation alive for as long as the controller stores the token.
7. Best Practices
Prefer Swift-native APIs when you do not need runtime lookup
KVC and KVO are powerful, but they trade away some type safety. If you can use direct properties, closures, notifications, or other Swift-native patterns, those are often easier to test and refactor.
struct Settings {
var theme: String = "light"
}
var settings = Settings()
settings.theme = "dark"This is safer than string-based property lookup because the compiler checks the property name for you.
Keep KVC/KVO boundaries small and explicit
Only expose the properties that truly need runtime access. The more of your model you expose to KVC/KVO, the easier it is for unrelated code to depend on fragile string keys.
final class Account: NSObject {
@objc dynamic private(set) var balance: Double = 0
func deposit(amount: Double) {
balance += amount
}
}This keeps mutation controlled while still allowing observation of the public state.
Store observation tokens and clean them up by ownership
Manage the observation token where the lifecycle is obvious, such as in the observing object. This prevents accidental early deallocation and makes teardown predictable.
final class ViewModel {
private var observation: NSKeyValueObservation?
func bind(person: NSObject) {
// Store the token on the instance that owns the observation.
}
}This practice avoids mysterious “it worked for one line and then stopped” behavior.
8. Limitations and Edge Cases
- Swift structs and enums do not participate in KVC/KVO in the usual way because they are value types and do not use Objective-C object identity.
- Property names are string-based in KVC, so renaming a property can silently break runtime code if you do not update all keys.
- Not every property can be observed. Computed properties, private storage, and non-Objective-C-exposed members often need special handling or cannot be observed directly.
- KVO can behave differently for inherited properties versus properties declared on the exact observed class.
- These APIs are best understood as interoperability tools, not the default way to model Swift state.
Warning: KVC and KVO can make code harder to reason about if you use them broadly. Limit them to integration points where runtime behavior is genuinely required.
9. Practical Mini Project
In this small example, a progress tracker updates a label-like string whenever the download percentage changes. It shows KVO in a realistic ownership pattern.
import Foundation
final class DownloadTask: NSObject {
@objc dynamic var progress: Double = 0.0
}
final class ProgressTracker {
private var observation: NSKeyValueObservation?
private let task: DownloadTask
init(task: DownloadTask) {
self.task = task
self.observation = task.observe(\.progress, options: [.initial, .new]) { task, change in
if let progress = change.newValue {
print("Progress is now \(progress * 100)%")
}
}
}
func simulateWork() {
task.progress = 0.25
task.progress = 0.75
task.progress = 1.0
}
}
let task = DownloadTask()
let tracker = ProgressTracker(task: task)
tracker.simulateWork()This example keeps the observable object and its observation token under clear ownership. It is a small but realistic pattern for runtime observation.
10. Key Points
- KVC reads and writes properties by string key at runtime.
- KVO notifies observers when a property changes.
- dynamic enables runtime dispatch, which KVO often relies on.
- These features usually require NSObject and @objc.
- Observation tokens must be retained or the observation stops.
- Pure Swift code usually has safer alternatives unless runtime interoperability is required.
11. Practice Exercise
- Create a class named WeatherReading with a temperature property that can be observed.
- Set up a KVO observation that prints the old and new temperature whenever it changes.
- Trigger at least two updates and keep the observation alive long enough to receive them.
- Make sure the property is compatible with KVO.
Expected output: You should see the initial temperature and each updated value printed in order.
Hint: Use NSObject, mark the property as @objc dynamic, and store the observation token in a variable that stays in scope.
Solution:
import Foundation
final class WeatherReading: NSObject {
@objc dynamic var temperature: Double = 18.0
}
let reading = WeatherReading()
let token = reading.observe(\.temperature, options: [.initial, .old, .new]) { _, change in
let oldValue = change.oldValue
let newValue = change.newValue
print("old: \(oldValue?.description ?? "nil"), new: \(newValue?.description ?? "nil")")
}
reading.temperature = 19.5
reading.temperature = 21.0
// Keep `token` alive until you no longer need updates.This solution works because the observable class participates in Objective-C runtime dispatch and the observation token remains alive.
12. Final Summary
KVC, KVO, and dynamic are interoperability tools that let Swift participate in runtime behavior from the Objective-C world. KVC reads and writes values by key name, KVO reports property changes, and dynamic ensures runtime dispatch where needed.
In modern Swift, these features should be used deliberately. They are most valuable at framework boundaries and in legacy integration code, while most app logic is easier to maintain with direct Swift properties and Swift-native observation patterns.
If you need deeper control over runtime behavior, the next step is to learn how @objc, NSObject, and Swift’s dispatch model work together in class inheritance and method overriding.