I know AppKit isn't thread safe and this may seem like an obvious thing in hindsight but I was expecting cocoa bindings controllers to dispatch updates to UI elements in the main thread. I was clearly wrong in that assumption.
In thinking about this problem, I started to play around with a controller object that would do exactly that... proxy observations but notify it's observers on the main thread. I put together a POC (I'm calling it AirLock for now) and it seems to work, but it was so easy that I'm nervous that I've overlooked something obvious.
The setup is simple. If a control was bound to SomeObject.someKeyPath:
o In IB, create an intance of AirLock and set the represented object to SomeObject
o Change the binding of the control to AirLock.representedObject.someKeyPath
This is the code for AirLock. Is there a better way?
@interface AirLock : NSObject
@property (nonatomic, retain) IBOutlet id representedObject;
@end
@interface AirLockContext : NSObject
@property (nonatomic, retain) NSObject *observer;
@property (nonatomic, retain) NSString *keyPath;
@property (nonatomic) void *context;
@end
@implementation AirLockContext
@end
@implementation AirLock
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
{
if ([keyPath hasPrefix:@"representedObject."])
{
NSString *subPath = [keyPath substringFromIndex:@"representedObject.".length];
AirLockContext *ctx = [[AirLockContext alloc] init];
ctx.observer = observer;
ctx.keyPath = keyPath;
ctx.context = context;
[_representedObject addObserver:self forKeyPath:subPath options:options context:(void *)CFBridgingRetain(ctx)];
}
else
{
[super addObserver:observer forKeyPath:keyPath options:options context:context];
}
}
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context
{
if ([keyPath hasPrefix:@"representedObject."])
{
// TODO
}
else
{
[super removeObserver:observer forKeyPath:keyPath context:context];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context
{
AirLockContext *ctx = (__bridge AirLockContext *)context;
dispatch_async(dispatch_get_main_queue(), ^{
[ctx.observer observeValueForKeyPath:ctx.keyPath ofObject:self change:change context:ctx.context];
});
}
@end