Leveraging Key-Value Observing (KVO) in Kotlin Multiplatform (KMP) for iOS

As an Android developer, venturing into iOS development with Kotlin Multiplatform (KMP), bridging the conceptual gap between familiar patterns and native iOS approaches can be a hurdle. This article explores utilizing Key-Value Observing (KVO), a fundamental mechanism for monitoring property changes in Cocoa, within a KMP iOS source set.

André Mion
ProAndroidDev

--

What is Key-Value Observing (KVO)?

Key-Value Observing is a pattern used in Cocoa frameworks to allow objects to be notified of changes to specified properties of another object. This mechanism is particularly handy when dealing with dynamic data changes in iOS applications.

KVO establishes a dependency between an observer and an observed object. The observer registers its interest in specific properties of the observed object. Whenever the observed property’s value changes, the observer is notified. This allows the observer to react accordingly, often by updating its state.

Observing an attribute of AVPlayer object in Swift:

internal class AudioPlayerImpl: NSObject, AudioPlayer {

private let player = AVPlayer()
private var observation: NSKeyValueObservation?

override init() {
super.init()

addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
}

override func observeValue(
forKeyPath keyPath: String?,
of: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
print("\(keyPath!) has been updated to: \(change![.newKey]!)")
}
}

KVO in KMP iOS

Using KVO in the KMP iOS source set might seem like a straightforward approach, but it comes with limitations:

  • Inheriting from NSObject is not possible if you are already inheriting from/implementing any Kotlin class/interface:
Mixing Kotlin and Objective-C supertypes is not supported
  • Overriding a function to observe changes is also not possible because observeValueForKeyPath is imported as Kotlin extension, which can't be overridden:
'observeValueForKeyPath' overrides nothing
What should we do?

🛟 Cinterops for the rescue!

The KMP plugin provides a DSL to define a custom definition file where we can declare our Objective-C protocol (interface) to be available in the Kotlin iOS source set.

package = platform.foundation
language = Objective-C
---
#import <Foundation/Foundation.h>

@protocol NSKeyValueObserving
@required
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context;
@end;
  • Define the interoperability using the KMP plugin DSL
kotlin {
androidTarget()

listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { target ->
target.binaries.framework {
baseName = "shared"
isStatic = true
}
target.compilations.getByName("main") {
// The default file path is src/nativeInterop/cinterop/<interop-name>.def
val nskeyvalueobserving by cinterops.creating
}
}
}
  • After compiling the KMP module, we have an interface where we can implement and observe NSObject changes:
internal class AudioPlayerImpl: AudioPlayer {

private val player = AVPlayer()

init {
player.addObserver(
observer = timeControlObserver,
forKeyPath = "timeControlStatus",
options = NSKeyValueObservingOptionNew,
context = null
)
}

private val timeControlObserver: NSObject = object : NSObject(), NSKeyValueObservingProtocol {

override fun observeValueForKeyPath(
keyPath: String?,
ofObject: Any?,
change: Map<Any?, *>?,
context: COpaquePointer?
) {
println("${keyPath} has been updated to: ${change!![NSKeyValueChangeNewKey]!!}")
}
}
}
}

💡 Remember to properly stop observing a property when you no longer need it to avoid memory leaks.

player.removeObserver(timeControlObserver, "timeControlStatus")

KVO provides a robust mechanism for monitoring property changes in iOS development. By understanding how to utilize KVO effectively within your KMP iOS source set, you can harness the full potential of this approach while leveraging the power of Kotlin Multiplatform.

Take a look at https://github.com/andremion/RetroBeat to see how KVO was used to listen for AVPlayerItem status updates.

Happy coding! 🤓

--

--

🇧🇷 Android Engineer living in 🇵🇹 • Full time Husband and Dad • Occasionally Drummer and Inline Skater… I write mostly about Android and Tech…