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.
Related
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.
I have a UIViewController which I'll call root, which is presenting (via modal segue) another UIViewController (firstChild), which is presenting (again via modal segue) a UINavigationController (topChild). In top child I do the following:
[root dismissViewControllerAnimated:NO completion:^{
[root performSegueWithIdentifier:#"ToNewFirstChild" sender:self];
}];
In iOS 7, the effect of this is that topChild remains on the screen until the segue to newFirstChild has been completed, and newFirstChild is then displayed (presented by root). I like that.
In iOS 8, the effect is that topChild is immediately removed from the screen, firstChild briefly is displayed and then removed, leaving root to be displayed until the segue has been completed, and newFirstChild is then displayed (presented by root). I don't like that.
If I choose to animate the dismissViewControllerAnimated:completion:, the following results happen: In iOS 7, topChild is dismissed with animation, without ever revealing firstChild (as advertised in the documentation), leaving root to be displayed until the segue has been completed; and in iOS8, topChild is immediately removed from the screen, leaving firstChild, which is dismissed with animation (contrary to the documentation!), again leaving root to be displayed until the segue has been completed.
Any idea how I can get the effect produced in iOS 7 (either with or without animation) in iOS 8? What am I missing?
The trouble you're dealing with here is that you're trying to call a method in the completion block of dismissViewControllerAnimated:completion of a View Controller that you're dismissing (and thus deallocating), thus the sporadic and unpredictable behaviour.
I'd recommend using something like NSNotificationCenter to post a notification when the view controller is being dismissed, and retain a handler in your parent (root) controller that can receive the notification.
In your root view controller, call this somewhere (maybe in prepareForSegue?):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(topChildWasDismissed:)
name:#"TopChildDismissed"
object:nil];
You'll also need a handler method in your root VC:
- (void)topChildWasDismissed {
[self performSegueWithIdentifier:#"ToNewFirstChild" sender:self];
}
Then in your top child:
[self dismissViewControllerAnimated:YES completion:^(void) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"TopChildDismissed" object:self];
}];
I've created an app that has two viewcontrollers. The app opens to a title screen (general UIViewController titled 'Title') with a segue connection to the second view that is a custom class (OSViewController titled 'MapView'). As it is, the app suspends when entered into the background state so it opens right where you left off which is typically in MapView.
I want to know what I need to do to have the app start at the title screen when it becomes active. Preferably, I'd like it to open to the title screen if it is inactive for more than 1 minute. From what I've been reading, it seems like I would make a call in applicationDidBecomeActive: method in my AppDelegate to code this in. Please provide me the code to put in the applicationDidBecomeActive: method (if that's the right place to put it) that will reopen my app to the title screen when transitioning from the inactive state to the active state. My app is almost finished but I'd like to fix this issue and I don't have a lot of experience dealing with app states. Thanks in advance for your time.
If you need more information just ask.
You can also register a class as an observer of the "didBecomeActive" notification. You should place this in the viewDidLoad or the init method of your class.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(willBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
In this case, willBecomeActive: is a method that you have defined in your class that get's called when the app becomes active again. That might look something like this:
- (void)willBecomeActive:(NSNotification *)notification {
if (self.navigationController.topViewController == self) {
[self.navigationController popToRootViewControllerAnimated:YES];
}
}
You'll also need to add this in your viewDidUnload method
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
EDIT:
Thanks #AMayes for the advice. I don't believe key/value observing is necessary in this instance.
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.