I have two NSManagedObject subclasses: Parent and Child. Parent contains many Child(ren) in an OrderedSet. When state changes in a Child I want the parent to know about it.
If I was programming in another language I might use events, having the Parent listening for events from each of its children, however given that target-action is limited to view components, all Objective C offers me is use of a global NSNotificationCenter. I definitely don't like the idea of a Model tapping into global notifications (listening directly via events is Ok in my book), so it seems my only alternative is Delegation. However using delegation between two NSManagedObjects seems like a dangerous idea, given the difficulty in ensuring one party does not lose its reference to the other.
Does anyone have any suggestions as to how I should be handling this?
Another option is key-value observing. Set it up like this:
const static void *kParentObservingChildSomePropertyContext = &kParentObservingChildSomePropertyContext;
[child addObserver: child.parent forKeyPath: #"someProperty" options: /*see below*/ context: kParentObservingChildSomePropertyContext];
the weird constant definition exists because every observing context should be unique so that you don't tread on superclass or subclass observation contexts. To see what options you need to set, consult the manual and compare with your specific needs. Now whenever that property on your child object changes, your parent will receive:
-(void)observeValueForKeyPath: (NSString *)path ofObject: (id)object change: (NSDictionary *)change context: (void *)context;
Check that you have the correct context for your observation, then handle the change. If you get a different context here, forward the message to super.
When you're done observing the path, you remove the parent as an observer of the child:
[child removeObserver: child.parent forKeyPath: #"someProperty" context: kParentObservingChildSomePropertyContext];
Use the same context pointer you used in -addObserver:forKeyPath:options:context so that the correct instance of the observation is removed.
However using delegation between two NSManagedObjects seems like a dangerous idea, given the difficulty in ensuring one party does not lose its reference to the other.
Delegating, observing and watching for notifications from specific objects all suffer from this problem. You need to make sure that the lifetime of your interest in the notifications matches the lifetimes of the objects involved, otherwise you could easily "leak" observation info or - and this is worse - send notifications to stale object pointers. None of these solutions is immune to that, though in the case of the Delegate pattern you can use a zeroing weak reference to ensure that when the parent object disappears, the child will no longer try to delegate to it.
I pick a field to watch in the view controller that will service the request...
[self addObserver:self forKeyPath:#"clientProgress.dateLastUpdated" options:0 context:nil];
I make sure in that same view controller that I remove the observer on dealloc (or whatever passes for dealloc in ARC).
- (void)dealloc {
[self removeObserver:self forKeyPath:#"clientProgress.dateLastUpdated"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqual:#"clientProgress.dateLastUpdated"]) {
// If this key path has changed, the browser needs to update its display.
NSError *error = nil;
if (![managedObjectContext save:&error]) {
UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:#"Save Error" message:#"Error saving inserted record, contact Tech Support" delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[dialog show];
[dialog release];
exit(-1); // Fail
}
if ( changeIsComingFromLibraryInsert ) {
}
[conditioningTableView reloadData];
}
// Essential to call super class implementation - NSArrayController relies heavily on KVO
//[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
Finally, when I want to call the observer, I merely update the date...
// force a save by setting the modified date and the preceeding ViewController with then save and reload from the calling view controller
// as there is an observer running watching this key value
clientProgress.dateLastUpdated = [NSDate date];
Related
I have the following code which I'm using to try and observe a variable, and regenerate text when it changes. But so far nothing happens:/
-(void)viewdidload{
float Index = 1;//declared in header locally
[self addObserver:self forKeyPath:#"Index" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self generateAdviceText];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(#"inside");
if([keyPath isEqualToString:#"Index"]){
NSLog(#"INDEX CHANGED");
[self generateAdviceText];
}
}
-(void)generateAdviceText{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *name = ((User *)appDelegate.users[self.currentUserIndex]).name;
NSString *clothWear = #"HAT";
NSMutableAttributedString *adviceString = [[NSMutableAttributedString alloc]initWithString:#"\"..."];
[adviceString appendAttributedString:[self boldString:name]];
[adviceString appendAttributedString:[self normalString:#", remember be extra aware of the cats today. The index is "]];
[adviceString appendAttributedString:[self boldString:[#(Index) stringValue]]];
[adviceString appendAttributedString:[self normalString:#", sth "]];
[adviceString appendAttributedString:[self boldString:clothWear]];
[adviceString appendAttributedString:[self normalString:#" sth "]];
[adviceString appendAttributedString:[self boldString:sth]];
[adviceString appendAttributedString:[self normalString:#"...\""]];
self.adviceLabel.attributedText = adviceString;
}
Can anybody spot my mistake? Thanks
Key-Value Observing does not generate change notifications for simple assignments to instance variables (or any other kind of variable).
In most cases, you need to call the setter of the property to generate a change notification for that object and that property.
At a low level, KVO only generates change notifications if the -willChange... and -didChange... methods from the NSKeyValueObserving informal protocol are called. Those methods are called in specific circumstances:
If a properly-named setter or collection mutation accessor for the property named by the key is called (assuming +automaticallyNotifiesObserversForKey: return true for that key). This is the best way.
If one of the -set... methods from the NSKeyValueCoding informal protocol, such as -setValue:forKey:, is called (again, assuming +automaticallyNotifiesObserversForKey: return true for that key).
If a collection proxy returned by one of the -mutable...ValueForKey: methods of the NSKeyValueCoding is sent mutation methods (again, depends on +automaticallyNotifiesObserversForKey:).
If some code (usually in the class defining the property) manually calls the -willChange... and -didChange... methods.
As Joe Shang noted, though, it's questionable for an object to observe itself. If an object wants to know when one of its properties has been changed, it should put the relevant code into its setter. Of course, it then has to modify its property exclusively using that setter, never by setting the instance variable directly (just like for KVO).
In the code you posted, though, it's just as well that you didn't get the KVO change notifications you expected. Your code would infinitely recurse until it crashed when it overflowed the stack. You attempted to make it so that changing the index calls -generateAdviceText and that -generateAdviceText changes the index.
What's the Index type? static variable or property? The keyPath in addObserver:forKeyPath:options:contexts: must be the property of some object (KVC compatible), you can read NSHipster's acticle and objc.io #7 to get more.
In the other hand, if Index is property, you don't need to use KVO here. You can override the setter method of Index and do some action when Index is update.
- (void)setIndex:(int)index
{
_index = index;
// add you generateAdviceText method here
}
Suppose i want to observe a property named 'isEnabled' on a property named 'controller' on self. AFAIK i have two options for installing such kind of observation:
1. [self.controller addObserver:self forKeyPath:#"isEnabled" options:0 context:nil];
2. [self addObserver:self forKeyPath:#"controller.isEnabled" options:0 context:nil];
I noticed the practical difference between the two approaches - on the second approach i will get a notification if the 'controller' object on self was replaced while with the first approach I will be notified only when 'isEnabled' property is changed on the same instance on which I installed the observation.
My question is where the hell is this documented if at all? I know it works but should I use it?
I could not find any mention of such behavior in Apple docs, though some other dudes mentioned it in forums. Any reference will be gladly accepted.
Thanks.
It's not just that you'll get a change notification if the controller property changes, it's that KVO will switch to tracking the isEnabled property of the new controller and stop tracking the isEnabled property of the old controller.
This is implicit in the notion of a key path. Key-Value Observing is built on top of Key-Value Coding. The Key-Value Coding Programming Guide says this about key paths in Key-Value Coding Fundamentals:
A key path is a string of dot separated keys that is used to specify a sequence of object properties to traverse. The property of the first key in the sequence is relative to the receiver, and each subsequent key is evaluated relative to the value of the previous property.
For example, the key path address.street would get the value of the address property from the receiving object, and then determine the street property relative to the address object.
The meaning of -addObserver:forKeyPath:options:context: is not "follow the key path to the final element and observe that property on the second-to-last object". It's "observe this key path of the receiver object". The key path is always considered starting from the receiver.
Put another way, for your second code example, it's not "observe the isEnabled property of the controller of self" (that's the meaning of your first example). The meaning is "observe the controller.isEnabled of self. Any time that the result of evaluating the expression [self valueForKeyPath:#"controller.isEnabled"] has or might have changed, notify me."
I know it works but should I use it?
Yes, you should use it. It's the intended meaning of the API. It's why the method describes its parameter as a key path rather than just a key.
on the second approach i will get a notification if the 'controller'
object on self was replaced
I would rephrase by saying that on the second approach you'll get notified if the controller object gets replaced or if its isEnabled property changes. In other word when controller.isEnabled changes (as explained by Ken's answer).
Check this example:
- (void)viewDidLoad {
[super viewDidLoad];
self.controller = [[ViewController2 alloc] init];
[self.controller addObserver:self forKeyPath:#"isEnabled" options:NSKeyValueObservingOptionNew context:nil];
[self addObserver:self forKeyPath:#"controller.isEnabled" options:NSKeyValueObservingOptionNew context:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.controller.isEnabled = !self.controller.isEnabled;
// replace controller
[self.controller removeObserver:self forKeyPath:#"isEnabled"];
self.controller = [[ViewController2 alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.controller.isEnabled = !self.controller.isEnabled;
});
});
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"KVO %d %# %p", self.controller.isEnabled, keyPath, self.controller);
}
We'll get 4 KVO notifications:
KVO 1 controller.isEnabled 0x7fbbc2e4b4e0 <-- These 2 fire together when we toggle isEnbled
KVO 1 isEnabled 0x7fbbc2e4b4e0 <-- So basically 1. and 2. behave the same
KVO 0 controller.isEnabled 0x7fbbc2e58d30 <---- controller was replaced
KVO 1 controller.isEnabled 0x7fbbc2e58d30 <---- Toggle on the new instance
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"];
}
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) {}
}
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
I'm trying to make a little binding system between my UILabel and my object Data using KVO. If my UI change, my data have to change, and if my data change my UI should refresh to display the new value.
The biggest issue I have is that I need to cast a custom object to a void* (context) with __bridge_retained void* - or CFBridgingRetain() - but I don't know where I should call CFBridgingRelease(). If call it in observeValueForKeyPath method I get a bad access error (I guess because my Reference Count to the object pointed by context is 0)
// viewDidLoad
// binding my label text with a custom data object
[self bindObject:_myLabel withPath:#"text" toObject:_user path:#"name"];
-(void) bindObject:(id)uiObj withPath:(NSString *)uiPath toObject:(id)dataObj path:(NSString *)dataPath
{
// custom object storing the object I want to bind and his path
PLSObjectPath* op = [[PLSObjectPath alloc] init];
op.theObj = dataObj;
op.thePath = dataPath;
PLSObjectPath* ob = [[PLSObjectPath alloc] init];
ob.theObj = uiObj;
ob.thePath = uiPath;
/* possible leak because I don't know where to call CFBridgingRelease */
[uiObj addObserver:self forKeyPath:uiPath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(op)];
[dataObj addObserver:self forKeyPath:dataPath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(ob)];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
PLSObjectPath *obj = (__bridge PLSObjectPath*) context;
PLSObjectPath* pairObj = [[PLSObjectPath alloc] init];
pairObj.theObj = object;
pairObj.thePath = keyPath;
// avoid infinite loop
[obj.theObj removeObserver:self forKeyPath:obj.thePath];
[obj.theObj setValue:change[#"new"] forKeyPath:obj.thePath];
[obj.theObj addObserver:self forKeyPath:obj.thePath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(pairObj)];
}
Traditionally users of this have used a static char * as the context parameter, so as to differentiate the different observeValueForKeyPath messages. That said, it should be possible to do something as you are attempting.
What I would suggest is to switch from a custom object to a Core Foundation one, where you can do you own memory management explicitly. Thus I'd suggest changing PLSObjectPath to CFDictionary. You can first create a NSDictionary, then "transfer" it to the CF domain with the appropriate cast, and pass that CFDictionary object for context (which is now a retained CF object). Recast it in observeValueForKeyPath to a CFDictionary, properly ARC cast it to a NSDictionary, use that, then it should get released if you've done the ARC correctly. This is all a well understood paradyme - moving objects in and out of ARC.
Another way you could do it is us a static NSMutableDictionary, and use the context pointer to go to a int value, which when converted to a NSNumber is the key to the dictionary. If all KVO occurs on the main thread, you don't need to protect the dictionary, but if not then you will need to put all access to the dictionary behind a serial dispatch queue that in forces serial access on one thread.
The memory leak is from [obj.theObj removeObserver:self forKeyPath:obj.thePath]. You are removing the observer but as the system doesn't treat the context as an object, it will not release it. Also, there is no way for you to get the context from the observed object itself.
At some point you will need to stop all observation to allow your view to be deallocated. This should happen from viewDid(Will)Disappear:. To be able to release the PLSObjectPath:s at that point, you will need to store them somewhere, possibly an NSMutableArray. If you will store these anyway for this purpose, you don't need to use __bridge_retained. Also, in this case your PLSObjectPath:s might/should contain a void* pointing to the other context. This way, you save the creation of PLSObject in observeValueForKeyPath:ofObject:change:context:.
Just a comment, you should start the KVO from viewWill(Did)Appear: and not from viewDidLoad. It gives you a better KVO start/stop management and also removes the unnecessary observation while your view is not on the screen.
Background:
All my OpenTok methods are in one ViewController that gets pushed into view, like a typical Master/detail VC relationship. The detailVC connects you to a different room depending on your selection. When I press the back button to pop the view away, I get a crash (maybe 1 out of 7 times):
[OTMessenger setRumorPingForeground] message sent to deallocated instance xxxxx
or
[OTSession setSessionConnectionStatus:]: message sent to deallocated instance 0x1e1ee440
I put my unpublish/disconnect methods in viewDidDisappear:
-(void)viewDidDisappear:(BOOL)animated{
//dispatch_async(self.opentokQueue, ^{
[self.session removeObserver:self forKeyPath:#"connectionCount"];
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
//});
[self doCloseRoomId:self.room.roomId position:self.room.position];
}
Here is a trace:
Here is the DetailViewController on Github: link here
How to reproduce:
Make a selection from the MasterVC, that takes you into the DetailVC which immediately attempts to connect to a session and publish
Go back to previous, MasterVC quickly, usually before the session has had an a chance to publish a stream
Try this several times and eventually it will crash.
If I slow down and allow the publisher a chance to connect and publish, it is less likely to cause a crash.
Expected result:
It should just disconnect from the session/unpublish and start a new session as I go back and forth between the Master/DetailVC's.
Other:
What is your device and OS version?
iOS 6
What type of connectivity were you on?
wifi
Zombies Enabled?
Yes
ARC Enabled?
Yes
Delegates set to nil?
Yes, as far as I know
Any help solving this crash would be greatly appreciated. Perhaps I'm missing something basic that I just can't see.
What seems to happen is that the OTSession object in the OpenTok library continues to to send messages to objects in that library that have since been deallocated by switching views. The library has a [session disconnect] method that works fine if you give it enough time, but it takes close to 2-3 seconds, and that's a long time to pause an app between views.
This might be a stupid question, but:
Is there anyway to stop all processes initiated by a certain VC?
Closing the session from viewWillDisappear() works if you can determine for sure that the view is going to be popped, not pushed or hidden. Some answers suggest putting this code in dealloc(). Regarding those suggestions, Apple says,
You should try to avoid managing the lifetime of limited resources using dealloc.
So, here is how you can determine for sure that your view will get popped. viewWillDisappear() is called when the view is popped from the stack, or is otherwise pushed somewhere else. This is the easiest way to determine which, and then unpublish/disconnect if it is truly popped. You can test for this with isMovingFromParentViewController. Also, here is where you can remove specific observers.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated]
// This is true if the view controller is popped
if ([self isMovingFromParentViewController])
{
NSLog(#"View controller was popped");
// Remove observer
[[NSNotificationCenter defaultCenter] removeObserver:self.session];
...
//dispatch_async(self.opentokQueue, ^{
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
//});
[self doCloseRoomId:self.room.roomId position:self.room.position];
}
else
{
NSLog(#"New view controller was pushed");
}
}
Ref: Testing for Specific Kinds of View Transitions
Looks like OpenTok have a bug with usage NSNotificationCenter inside of OTSession and OTMessenger classes. You can see these classes in call-stack are separated with NSNotificationCenter calls:
You can manually unsubscribe your OTSession object when dealloc (hope OpenTok uses defaultCenter):
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self.session];
}
You need to check if this code (dealloc) is really executed. If not - you need to fix problem of UIViewController deallocation. A lot of other answers contains tips how to help UIViewController to be deallocated.
-(void)viewDidDisappear:(BOOL)animated is called whenever the view is hidden, not only when it is popped from the view stack.
So if you push a view over it, viewWillDisappear will be called and your objects deleted.
This is specially problematic if you load these same objects from viewDidLoad: instead of viewDidAppear:.
Perhaps you should put your unpublish/disconnect code in -(void)dealloc.
This is what Apple suggests:
-(void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
But this is only the last resort to remove observers, still often a good habit to always add it to make sure everything is cleand up on dealloc to prevent crashes.
It's still a good idea to remove the observer as soon as the object is no longer ready (or required) to receive notifications.
I most of the time put such a code in the viewWillDisappear, but I guess that doesn't really matter.
I believe the issue is that your session delegate is not set to nil. Just add the following in your viewDidDisappear:
self.session.delegate=nil;
You must call [super viewDidDisappear:animate]; at the beginning. May be it will fix your issue.
And better cleanup your session and subscriber in dealloc method:
- (void) dealloc {
[self.session removeObserver:self forKeyPath:#"connectionCount"];
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
[self doCloseRoomId:self.room.roomId position:self.room.position];
//[super dealloc]; //for non-ARC
}
According to the stack trace you have posted, the notification center reaches out to an OTSession instance that is still alive. Afterwards, this instance provokes a crash calling methods on deallocated objects.
Adding to that the two different deallocated instance messages, we know there are asynchronous events occuring after the death of some objects that trigger the random crash you are having.
As ggfela suggested, you should make sure to nil out the delegates you have connected to the OpenTok framework. I strongly suggest you do that in the dealloc method as we want to make sure that after that point, no one has any dangling references to your object :
- (oneway void)dealloc
{
self.session.delegate = nil;
self.publisher.delegate = nil;
self.subscriber.delegate = nil;
}
Another odd thing in the code is that your handler for sessionDidConnect: creates a new dispatch_queue every time it is being called in order to call doPublish:. This means that you have concurrent threads sharing the SROpenTokVideoHandler instance which makes it prone to race conditions.