I have a model object and a window controller. I would like them to be able to communicate via notifications. I create both during the App Delegate's -applicationDidFinishLaunching: method. I add the observers after the window controller's window is loaded, like this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
WordSetWindowController* windowController = [[WordSetWindowController alloc] initWithWindowNibName:#"WordSetWindowController"];
model = [[WordSetModel alloc] init];
NSWindow* window = windowController.window;
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_GeneratingPairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_ProcessingPairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_UpdatePairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_Complete
object:model];
[model initiateSearch];
}
The -iniateSearch method kicks off some threads to do some processor-intensive calculations in the background. I'd like those threads to send notifications so I can update the UI while processing is occurring. It looks like this:
- (void)initiateSearch;
{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_GeneratingPairs
object:self];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ... do the first part of the calculations ...
// Notify the UI to update
self->state = SearchState_ProcessingPairs;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_ProcessingPairs
object:self];
});
// ... Do some more calculations ...
// Notify the UI that we're done
self->state = SearchState_Idle;
dispatch_sync(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_Complete
object:self];
});
});
}
The first notification works properly, but none of the notifications that happen in the dispatch_async() call ever cause the notification handler to be called. I've tried calling -postNotificationName:: both on the background thread and the UI thread (both by using dispatch_async(dispatch_get_main_queue(),...) and by calling -performSelectorOnMainThread:::) and neither had any effect.
Curious, I added a call via an NSTimer that waits 5 seconds after the big dispatch_async() call at the end of -initiateSearch: and found that even though that all occurs on the main UI thread, it also does not fire the notification handler. If I simply call postNotification::: immediately after the dispatch_async() call returns, it works properly, though.
From this, I'm concluding that the observers are somehow getting removed from the notification center, despite the fact that my code never calls -removeObserver:. Why does this happen, and how can I either keep it from happening or where can I move my calls to -addObserver so that they aren't affected by this?
Is suspect your window controller is getting deallocated.
You assign it to WordSetWindowController* windowController, but that reference disappears at the end of -applicationDidFinishLaunching:, and likely your window controller with it.
Since the window itself is retained by AppKit while open, you end up with an on-screen window without a controller. (Neither NSWindow nor NSNotificationCenter maintain strong references to its controller/observers.)
The initial notification works because those are posted before -applicationDidFinishLaunching: ends and/or the autorelease pool for that event is drained.
Create a strong reference to your window controller in your application delegate, store the window controller's reference there, and I suspect everything will work as advertised.
Something very similar happened to me with a window controller that was also a table delegate; the initial setup and delegate messages would work perfectly, but then later selection events mysteriously disappeared.
Related
I have a custom in app notification appear at the top of any view controller on the screen when certain things happen.
Tapping on it triggers a notification:
[[NSNotificationCenter defaultCenter] postNotificationName:#"DidTapOnNotification" object:nil];
The observer for that specific notification is the root navigation controller for my application, which I subclassed. I addObserver in viewDidLoad. This notification is always received, and the code I run in response is:
[CATransaction begin];
[CATransaction setCompletionBlock:^{ // this is called when the popToRootViewController animation completes
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"NavigateToController" object:nil userInfo:notification.userInfo];
});
}];
[self popToRootViewControllerAnimated:YES];
[CATransaction commit];
I added a delay (dispatch_after) arbitrarily to see whether I just had to give my root view controller time to appear (i confirmed that it appeared before the 2 seconds are up).
Now, in my root view controller I again, add it as an observer for the NavigateToGroup notification. I.e. I call [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(openController:) name:#"NavigateToController" object:nil]; in viewDidLoad.
The problem is that the selector (i.e. the openController: method) is not always called. The only time it's called is when I'm already on that controller (the root controller) when I tap on the in app notification, then it works as expected. If I have other controllers on the navigation stack, tapping will popToRoot as expected, but then the method openController: will never get called even though I'm sure the notification gets posted (and i'm sure the view is appeared when it does).
Does anyone know what's going on here?
Or conversely, can anyone recommend a better way of handling this?
Turns out HariKrishnan.P's comment had some truth to it. NSNotifications don't seem to be posted if the view controller that is sending the message has already been popped. (Not sure if this is always the case but definitely seemed to be the case here)
I ended up refactoring the code to only send one initial notification then bypassing this by using delegation and calling my method explicitly through it rather then relying on on an NSNotification, which is probably a better solution anyways.
When my application is interrupted, such as receiving a phone call, screen locked, or switching applications, I need it to respond differently depending on which view/viewcontroller is on screen at the time of the interruption.
in my first view controller, we'll call it VCA, I have this
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(doSomething)
name:UIApplicationWillResignActiveNotification
object:NULL];
-(void)doSomething{
//code here
};
In VCB I have
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(doSomethingElse)
name:UIApplicationWillResignActiveNotification
object:NULL];
-(void)doSomethingElse{ //code here };
but if VCB is on screen, or any subsequent view controller (vcc, vcd, vce), and the screen is locked, it will only respond to the doSomething method defined in VCA. Even if I don't have the UIApplicationWillResignActiveNotification in one of the view controllers that comes after VCA, it will still respond to the doSomethign method defined in VCA.
Is there any way I can make my application respond differently depending on which view is on screen when it goes into the background?
This works for me in applicationDidEnterBackground
if ([navigationViewController.visibleViewController isKindOfClass:[YourClass class]]) {
//your code
}
Are you saying your doSomethingElse function is never called? Are you sure of this, maybe it is getting called in addition to doSomething? I think so.
In which case in doSomething and doSomethingElse you could add a check as the first line to ignore the notification if not currently loaded:
if ([self isLoaded] == NO)
return;
How about you check the current visibleViewController when you received the notification? If it matches with your receiver than perform the action(s), otherwise ignore it.
I have a Single View Application. When I hit the home button and ‘minimise’ the application I want to be able to execute code when the user reopens it.
For some reason viewDidAppear and viewWillAppear do not execute when I minimise and reopen the application.
Any suggestions?
Thanks in advance
sAdam
You can either execute code in the app delegate in
- (void)applicationDidBecomeActive:(UIApplication *)application
or register to observe the UIApplicationDidBecomeActiveNotification notification and execute your code in response.
There is also the notification UIApplicationWillEnterForegroundNotification and the method - (void)applicationWillEnterForeground:(UIApplication *)application in the app delegate.
To hook up notifications add this at an appropriate point
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(didBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
Define a the corresponding method
- (void)didBecomeActive:(NSNotification *)notification;
{
// Do some stuff
}
Then don't forget to remove yourself an observer at an appropriate point
[[NSNotificationCenter defaultCenter] removeObserver:self];
Discussion
You most likely only want your viewController to respond to events whilst it is the currently active view controller so a good place to register for the notifications would be viewDidLoad and then a good place to remove yourself as an observer would be viewDidUnload
If you are wanting to run the same logic that occurs in your viewDidAppear: method then abstract it into another method and have viewDidAppear: and the method that responds to the notification call this new method.
This is because since Apple implemented "Multitasking", apps are completely reloaded when you start them again, just as if you had never closed them. Because of this, there is no reason for viewDidAppear to be called.
You could either implement
- (void)applicationWillEnterForeground:(UIApplication *)application
and do there what ever you want. Or you register for the notification UIApplicationWillEnterForegroundNotification in your view controller. Do this in viewDidLoad:
[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(myAppWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification object:nil];
And of course implement the specified selector and do there what you want.
I am not sure how the answer by #Paul.s performs the OP request since registering UIApplicationDidBecomeActiveNotification will be executed twice:
When launching the app
When application goes into the background
A better practice will be to decouple those events into 2 different notifications:
UIApplicationDidBecomeActiveNotification:
Posted when the app becomes active.
An app is active when it is receiving events. An active app can be said to have focus. It gains focus after being launched, loses focus when an overlay window pops up or when the device is locked, and gains focus when the device is unlocked.
Which basically means that all logic related to "when application launched for the first time"
UIApplicationWillEnterForegroundNotification:
Posted shortly before an app leaves the background state on its way to becoming the active app.
Conclusion
This way we can create a design that will perform both algorithms but as a decoupled way:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(yourMethodName1) name:UIApplicationWillEnterForegroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(yourMethodName2) name:UIApplicationDidBecomeActiveNotification object:nil];
This because you don't redraw your view. Use applicationWillEnterForeground in the AppDelegate instead. This should work fine for you.
I have a observer problem with NSNotificationCenter in my app.
My AppDelegate class has 2 service class to get data by url which called ExhibitionService & NewsService.
This 2 service class uses one Queueloader class in itself.
When I wrote 2 observer to listen service load operations in my appdelegate class, it returns error and crashes.
APP DELEGATE CLASS
ExhibitionLoaderService *exhibitionService = [[ExhibitionLoaderService alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(exhitibionServiceComplete :) name:**CserviceComplete** object:nil];
[exhibitionService load];
NewsLoaderService *newsService = [[NewsLoaderService alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(newsServiceComplete :) name:**CserviceComplete** object:nil];
[newsService load];
ExhibitionLoaderService.m & NewsLoaderService has the same method
-(void)load
{
Queueloader *que = [[Queueloader alloc] initWithPath:CExhibitionURLPath isVerbose:NO];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didQueComplete:) name:CdidQueueloaderComplete object:nil];
[que startOperation];
[que release];
}
ERROR I GOT
[[NSNotificationCenter defaultCenter] postNotificationName:**CdidQueueloaderComplete** object:results];
2 service class has CdidQueueloaderComplete... Problem is about observers but how? what?
PS. Program received signal EXC_BAD_ACCESS.
Thanks.
There's no problem with having multiple observers of the same notification. The problem you describe sounds a lot like it's related to the lifetime of your observers.
If you deallocate an object that's still registered to listen to notifications, the NSNotificationCenter doesn't know about that. If a notification comes in in the future, the center will forward it to the object it thinks is still listening (but that has gone away), and you get a crash.
The solution to this problem is to ensure that your object is removed as an observer before it is destroyed. There are two ways to do this:
often you'll know when an object should start or stop listening to notifications, and you can make sure you remove it as an observer when it should stop (for example, perhaps view controllers should start listening for model updates when their views appear and stop listening when their views disappear)
other times, an object can look after its own notification lifecycle: perhaps you can start listening in an initialiser and stop listening in -dealloc.
Whatever way you do it, you need to balance adding observers and removing observers so that when an object goes away it's no longer registered with the notification center.
In applications like Foursquare when I click the Home button the application goes to background. Then when I click on its icon, it loads back the content on the screen.
When I send my app to background and then I recall it back, it doesn't load back the content to the screen. I have entered my code in the viewDidAppear method but it is not executed.
How is it possible to load the application content when it becomes active?
You need to respond to - (void)applicationDidBecomeActive:(UIApplication *)application or - (void)applicationWillEnterForeground:(UIApplication *)application or the equivalent UIApplication notifications. The UIViewController lifecycle calls like viewDidAppear aren't triggered by app lifecycle transitions.
smparkes suggestion is right. You could register for UIApplicationDidBecomeActiveNotification or UIApplicationWillEnterForegroundNotification. These notifications are called after those method (the ones smparkes wrote) are called. In the handler for this notification do what you want. For example in viewDidLoad for your controller register the following notification:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doUpdate:)
name:UIApplicationDidBecomeActiveNotification object:nil];
Do not forget to remove in dealloc:
[[NSNotificationCenter defaultCenter] removeObserver:self];
Finally, doUpdate method could be the following
-(void)doUpdate:(NSNotification*)note
{
// do your stuff here...
}
I suggest you to read UIApplicationDelegate class reference. In particular read about Monitoring Application State Changes.
Hope it helps.
Suppose you want to listen to UIApplicationDidBecomeActiveNotification,here is the ObjC code that might help you.
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// custom code goes here.
}];