NSProxy
seems to work very well as stand-in objects for those that don't yet exist. For example.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
The above code will transparently pass any method invocation to the target that the proxy represents. However, it doesn't seem to handle KVO observations and notifications on the target. I tried to use a NSProxy
subclass as standing for objects to be passed to NSTableView
, but I'm getting the following error.
Cannot update for observer <NSAutounbinderObservance 0x105889dd0> for
the key path "objectValue.status" from <NSTableCellView 0x105886a80>,
most likely because the value for the key "objectValue" has changed
without an appropriate KVO notification being sent. Check the
KVO-compliance of the NSTableCellView class.
Is there a way to make transparent NSProxy
that is KVO compliant?
I don't have the exact same use case (no bindings) of OP but mine was similar: I am creating an NSProxy subclass that presents itself as another object that is actually loaded from a server. During the load, other objects can subscribe to the proxy and the proxy will forward the KVO as soon as the object arrives.
There is a simple
NSArray
property in the proxy that records all observers. Until the real object is loaded, the proxy returnsnil
invalueForKey:
. When therealObject
arrives, the proxy callsaddObserver:forKeyPath:options:context:
on the real object and then, through the magic of the runtime, walks through all properties ofrealObject
and does this:This seems to work, at least I haven't gotten any KVO compliance errors yet. It does make sense though, first all properties are nil and then they change from nil to the actual value. It is all like ipmcc said in his first statement above, so this post is just a confirmation! Note that the second surrogate that he proposed actually isn't needed, you just have to keep track of observers yourself.
The crux of the issue is that the guts of Key-Value Observing lives in
NSObject
, andNSProxy
doesn't inherit fromNSObject
. I'm reasonably confident that any approach will require theNSProxy
object to keep its own list of observances (i.e. what outside folks are hoping to observe about it.) This alone would add considerable weight to your NSProxy implementation.Observe the target
It looks like you've already tried having observers of the proxy actually observe the real object -- in other words, if the target were always populated, and you simply forwarded all invocations to the target, you would also be forwarding
addObserver:...
andremoveObserver:...
calls. The problem with this is that you started out by saying:For completeness, I'll describe some of the guts of this approach and why it can't work (at least for the general case):
In order for this to work, your
NSProxy
subclass would have to collect invocations of the registration methods that were called before the target was set, and then pass them through to the target when it gets set. This quickly gets hairy when you consider that you must also process removals; you wouldn't want to add an observation that was subsequently removed (since the observing object could have been dealloc'ed). You also probably don't want your method of tracking observations to retain any of the observers, lest this create unintended retain cycles. I see the following possible transitions in target value that would need to be handlednil
on init, becomes non-nil
laternil
, becomesnil
laternil
, then changes to another non-nil
valuenil
(not on init), becomes non-nil
later...and we run into problems right away in case #1. We would probably be all right here if the KVO observer only observed
objectValue
(since that will always be your proxy), but say an observer has observed a keyPath that goes through your proxy/real-object, sayobjectValue.status
. This means that the KVO machinery will have calledvalueForKey: objectValue
on the target of the observation and gotten your proxy back, then it will callvalueForKey: status
on your proxy and will have gottennil
back. When the target becomes non-nil
, KVO will have considered that value to have changed out from under it (i.e. not KVO compliant) and you'll get that error message you quoted. If you had a way to temporarily force the target to returnnil
forstatus
, you could turn that behavior on, call-[target willChangeValueForKey: status]
, turn the behavior off, then call-[target didChangeValueForKey: status]
. Anyway, we can stop here at case #1 because they have the same pitfalls:nil
won't do anything if you callwillChangeValueForKey:
on it (i.e. the KVO machinery will never know to update its internal state during a transition to or fromnil
)nil
from valueForKey: for all keys seems like a pretty onerous requirement, when the stated desire was a "transparent proxy".nil
target? do we keep those values around? waiting for the real target? do we throw? Huge open issue.One possible modification to this approach would be to use a surrogate target when the real target is
nil
, perhaps an emptyNSMutableDictionary
, and forward KVC/KVO invocations to the surrogate. This would solve the problem of not being able to meaningfully callwillChangeValueForKey:
onnil
. All that said, assuming you've maintained your list of observations, I'm not optimistic that KVO will tolerate the following sequence that would be involved with setting the target here in case #1:-[proxy addObserver:...]
, proxy forwards to dictionary surrogate-[surrogate willChangeValueForKey:
] because target is being set-[surrogate removeObserver:...
] on surrogate-[newTarget addObserver:...]
on new target-[newTarget didChangeValueForKey:
] to balance call #2It's not clear to me that this won't also lead to the same error. This whole approach is really shaping up to be a hot mess, isn't it?
I did have a couple alternate ideas, but #1 is fairly trivial and #2 and #3 aren't simple enough or confidence-inspiring enough to make me want to burn the time to code them up. But, for posterity, how about:
1. Use
NSObjectController
for your proxySure, it gums up your keyPaths with an extra key to get through the controller, but this is sort of
NSObjectController's
whole reason for being, right? It can havenil
content, and will handle all the observation set up and tear-down. It doesn't achieve the goal of a transparent, invocation forwarding proxy, but for example, if the goal is to have a stand-in for some asynchronously generated object, it would probably be fairly straightforward to have the asynchronous generation operation deliver the final object to the controller. This is probably the lowest-effort approach, but doesn't really address the 'transparent' requirement.2. Use an
NSObject
subclass for your proxyNSProxy's
primary feature isn't that it has some magic in it -- the primary feature is that it doesn't have (all) theNSObject
implementation in it. If you're willing to go to the effort to override allNSObject
behaviors that you don't want, and shunt them back around into your forwarding mechanism, you can end up with the same net value provided byNSProxy
but with the KVO support mechanism left in place. From there, it's a matter of your proxy watching all the same key paths on the target that were observed on it, and then rebroadcastingwillChange...
anddidChange...
notifications from the target so that outside observers see them as coming from your proxy....and now for something really crazy:
3. (Ab)Use the runtime to bring the
NSObject
KVC/KVO behavior into yourNSProxy
subclassYou can use the runtime to get the method implementations related to KVC and KVO from
NSObject
(i.e.class_getMethodImplementation([NSObject class], @selector(addObserver:...))
), and then you can add those methods (i.e.class_addMethod([MyProxy class], @selector(addObserver:...), imp, types)
) to your proxy subclass.This will likely lead to a guess-and-check process of figuring out all the private/internal methods on
NSObject
that the public KVO methods call, and then adding those to the list of methods that you wholesale over. It seems logical to assume that the internal data structures that maintain KVO observances would not be maintained in ivars ofNSObject
(NSObject.h
indicates no ivars -- not that that means anything these days) since that would mean that everyNSObject
instance would pay the space price. Also, I see a lot of C functions in stack traces of KVO notifications. I think you could probably get to a point where you had brought in enough functionality for the NSProxy to be a first-class participant in KVO. From that point forward, this solution looks like theNSObject
based solution; you observe the target and rebroadcast the notifications as if they came from you, additionally faking up willChange/didChange notifications around any changes to the target. You might even be able to automate some of this in your invocation forwarding mechanism by setting a flag when you enter any of the KVO public API calls, and then attempting to bring over all methods called on you until you clear the flag when the public API call returns -- the hitch there would be trying to guarantee that bringing over those methods didn't otherwise ruin the transparency of your proxy.Where I suspect this will fall down is in the mechanism whereby KVO creates dynamic subclasses of your class at runtime. The details of that mechanism are opaque, and would probably lead to another long train of figuring out private/internal methods to bring in from
NSObject
. In the end, this approach is also completely fragile, lest any of the internal implementation details change....in conclusion
In the abstract, the problem boils down to the fact that KVO expects a coherent, knowable, consistently updated (via notifications) state across it's key space. (Add "mutable" to that list if you want to support
-setValue:forKey:
or editable bindings.) Barring dirty tricks, being first class participants means beingNSObjects
. If one of those steps in the chain implements it's functionality by calling through to some other internal state, that's its prerogative, but it'll be responsible for fulfilling all its obligations for KVO compliance.For that reason, I posit that if any of these solutions are worth the effort, I'd put my money on the "using an
NSObject
as the proxy and notNSProxy
." So to get to the exact nature of your question, there may be a way to make anNSProxy
subclass that is KVO compliant, but it hardly seems like it would worth it.