How can I get key value observation info in Swift?

NSObject.observationInfo is a pointer that identifies information about all of the observers that are registered with the observed object. In objc, I can easily print it with %@:

NSObject *observer = [NSObject new];
NSObject *observed = [NSObject new];
[observed addObserver:observer forKeyPath:@"hash" options:0 context:nil];
NSLog(@"%@", observed.observationInfo);
/*
 <NSKeyValueObservationInfo 0x10045d560> (
 <NSKeyValueObservance 0x10045d300: Observer: 0x10045bd70, Key path: hash, Options: <New: NO, Old: NO, Prior: NO> Context: 0x0, Property: 0x10045bfe0>
 )
 */

In Swift however, it was represented by UnsafeMutableRawPointer. I have no idea how to handle this:

let observer = NSObject()
let observed = NSObject()
observed.addObserver(observer, forKeyPath: "hash", options: [], context: nil)
let info = observed.observationInfo!

print(info)
// 0x00007fbc93711510

print(info.assumingMemoryBound(to: NSObject.self).pointee)
// error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x18).

print(UnsafePointer<NSObject>(OpaquePointer(info)).pointee)
// error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x18).

String(format: "%@", info)
// error: argument type 'UnsafeMutableRawPointer' does not conform to expected type 'CVarArg'

String(format: "%@", info as! CVarArg)
// error: Execution was interrupted, reason: signal SIGABRT.

Thanks, but I want to print KVO informations of arbitrary object, for debugging.

KVO relationships are stored in a global dictionary. therefore, When the same address is reused, an object could have an observer right after its creation. observationInfo is helpful to find observer that was not properly removed.

Basically, What I'm trying to do is cast UnsafeMutableRawPointer(void*) to concrete type. Since [(id)info isKindOfClass:[NSObject class]] == YES, I thought info.assumingMemoryBound(to: NSObject.self).pointee would work.

1 Like

Try the Unmanaged type.

let info = Unmanaged<AnyObject>
  .fromOpaque(observed.observationInfo!)
  .takeUnretainedValue()

Or add a breakpoint in Xcode, and edit it to use:

  • a Debugger Command action, and
  • the Automatically continue after evaluating actions option.
expression --language objc --object-description -- [observed observationInfo]
1 Like

Awesome! It works.

I found unsafeBitCast(info, to: AnyObject.self) also works. but why? what's the difference between UnsafePointer.pointee, Unmanaged.takeUnretainedValue and unsafeBitCast?

// <Foundation/NSKeyValueObserving.h>

@interface NSObject(NSKeyValueObservingCustomization)

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

@end

To send the description or debugDescription message to the observationInfo, you need a Swift reference type instead of an unsafe pointer type.

In your original example:

  • the observed.observationInfo! is of type UnsafeMutableRawPointer;
  • the assumingMemoryBound(to:) method will return an UnsafeMutablePointer<NSObject>;
  • the pointee property will dereference the pointer. This is incorrect in both Swift and Objective-C.

Using unsafeBitCast(_:to:) with your example is like a C++ static_cast or reinterpret_cast between two pointer types. It should be OK for debug-only code.

Using the Unmanaged type is like an Objective-C bridged cast.

Thank you for your kind response. I understand. In some sense, Unmanaged<AnyObject> represent a pointer, and UnsafePointer<AnyObject> represent a double pointer.

Yes, I think you're right. I mistranslated between Objective-C and Swift. Usually, a pointer to an Objective-C pointer uses an AutoreleasingUnsafeMutablePointer.

So your original example was assuming that void * was an NSObject **, and dereferencing to NSObject *. But it would actually end up with the isa_t value of the instance.

By the way, the debugger command po observed.observationInfo might also work, but I don't know how the default language is chosen.