You will not find this in any documentation (that is, I have no “evidence”), but I can tell you from a painful personal experience, consisting of many days (if not weeks) of debugging, that this kind of failure is caused by adding / removing observers for a property inside the KVO notification handler for this property. (The presence of NSKeyValuePopPendingNotificationPerThread in the stack trace is a “smoking gun” in my experience.) I have also empirically observed that the order in which observers of this property are notified is non-deterministic, so even if adding or removing observers inside notification handlers works for a while, it may arbitrarily fail under different circumstances. (I assume there is an unordered data structure in the guts of the KVO that can be listed in different orders, possibly based on the numerical value of the pointer or something similar.) In the past, I worked around this by posting NSNotification just before / after installation properties to give observers the ability to add / remove themselves. This is a clumsy scheme, but it is better than a failure (and allows me to continue to use other things that rely on KVO, like bindings).
In addition, as an aside, I noticed in the code you posted that you do not use contexts to determine your observations, and you do not call super observeValueForKeyPath:... in your implementation. Both of these things can lead to subtle, hard-to-diagnose errors. A more bulletproof pattern for KVO is as follows:
static void * const MyAdjustingFocusObservationContext = (void*)&MyAdjustingFocusObservationContext; static void * const MyAdjustingExposureObservationContext = (void*)&MyAdjustingExposureObservationContext; - (void)focusAtPoint { // ... other stuff ... [device addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:MyAdjustingFocusObservationContext]; [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:MyAdjustingExposureObservationContext]; // ... other stuff ... } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == MyAdjustingFocusObservationContext) { // Do stuff } else if (context == MyAdjustingExposureObservationContext) { // Do other stuff } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
EDIT: I wanted to see if I could help in this particular situation. From the code and your comments, I understand that you are looking for these observations in order to effectively be a single shot. I see two ways to do this:
A simpler and bulletproof approach would be to ensure that this object always addObserver:... capture device (ie addObserver:... when initialized, removeObserver:... when you release it), but then the “gate” of the behavior, using a pair of ivars called waitingForFocus and waitingForExposure . In -focusAtPoint , where you currently addObserver:... instead, set ivars to YES . Then in observeValueForKeyPath:... perform the action only if these ivars are YES , and then instead of removeObserver:... just set ivars to NO . This should have the desired effect, without requiring you to add and remove an observation each time.
Another approach that I was thinking about is to call removeObserver:... "later" with a GCD. Therefore, you would removeObserver:... as follows:
dispatch_async(dispatch_get_main_queue(), ^{ [device removeObserver:self forKeyPath:keyPath context:context]; });
This will cause the call to be made elsewhere in the run loop after the notification process is completed. This is slightly smaller than bulletproof because it does not guarantee that a notification will not be issued a second time before a deferred call. In this regard, the first approach is more strictly "correct" in achieving the desired one-time behavior.
EDIT 2: I just couldn't let that happen. :) I understood why you are falling apart. I noticed that setting exposureMode , while in the KVO handler for adjustingExposure ends up causing another notification for adjustingExposure , and so the stack explodes until your process is killed. I managed to get it to work by wrapping the observeValueForKeyPath:... , which handles the changes in adjustingExposure to dispatch_async(dispatch_get_main_queue(), ^{...}); (including a possible call to removeObserver:... ). After that, he worked for me and definitely blocked the exposure and focus. However, as I mentioned above, it would probably be better to handle it with ivars to prevent recursion, rather than with an arbitrary delay dispatch_async() .
Hope this helps.