Reputation: 38142
I am seeing some random crashes with my app (although not reproducible when I run through same steps). I am observing the contentOffset property of the scrollview to take some action when it changes.
But I am getting below exception (randomly) with my below code of KVO registration and de-registration.
Is there any safe check that can be applied here.
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <MyPagingController 0x1f05e460> for the key path "contentOffset" from <UIScrollView 0x1f0a8fd0> because it is not registered as an observer.'
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)viewWillDisappear:(BOOL)iAnimated {
[super viewWillDisappear:iAnimated];
[self.scrollView removeObserver:self forKeyPath:@"contentOffset"];
}
Upvotes: 0
Views: 1494
Reputation: 1682
As much as I love KVO, this need to balance observing and removing is a real issue. You will also get crashes if you nil an object that was KVO observing without telling the sending class to de-register you and the class then tries to send an update to the receiver object. Oops! Bad Access!
As there is no official way to ping to see if you have added an observer (which is ridiculous given that a huge part of what goes on in OS is based on KVO), I just use a simple flag system to ensure that nothing gets added more than once. In the example below, I have a bool flag to track the observer status.
@interface RepTableViewController ()
@property (nonatomic, assign) BOOL KVOSet;
@end
#pragma mark - KVOObserving
- (void)_addKVOObserving
{
//1. If the BOOL flag shows we are already observing, do nothing.
if (self.KVOSet) return;
//2. Set the flag to YES BEFORE setting the observer as there's no guarantee it will happen immediately.
self.KVOSet = YES;
//3. Tell your class to add you up for the object / property you want to observe.
[[ELRepresentativeManager sharedManager]addObserver:self
forKeyPath:@"myRepresentative"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)_removeKVOObserving
{
//1. Do nothing if we have not set an observer
if (!self.KVOSet) return;
//2. If we have an observer, set the BOOL flag to NO before removing
self.KVOSet = NO;
//3. Remove the observer
[[ELRepresentativeManager sharedManager] removeObserver:self
forKeyPath:@"myRepresentative" context:nil];
}
Yes, using flags to check state is frowned upon by the coding-police. But there really is no other way to be sure except to bean count.
Whatever you do, remember that some classes need to observe even after viewWillDisappear is called (but the view still exists somewhere in the hierarchy), so you can't just do the observe/remove from viewWillAppear/WillDisappear trick. You may need to use delegate callbacks to the object that created the view (and will destroy it) to ensure you never leave a KVO calling class hanging. Having said that, I'm no expert in this stuff, so I'm sure there are people who can do this with way more finesse than my patented bean-counting lol
Upvotes: 0
Reputation: 23624
Your unsubscribing code somehow gets hit more often than the subscribing code. Unfortunately KVO does not handle this nicely and the failure throws an exception rather than just doing nothing as you would expect. You either need to make sure it only gets hit once, or at least catch the exception like this:
@try
{
[self.scrollView removeObserver:self forKeyPath:@"contentOffset"];
}
@catch (NSException * __unused exception) {}
}
Upvotes: 1