viewDidAppear only gets called under certain circumstances? - ios

I have this problem that has popped up out of the blue, I have checked all of the changes I have made in the past week and nothing seems to explain why my viewDidAppear only gets called when pushing to my VC under certain circumstances. These are the two ways I push to my VC.
-(void)receiveOnHostRoomConnectNotification:(NSNotification *)notificaiton
{
[[NSNotificationCenter defaultCenter]removeObserver:self name:kInitialize object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self name:kOnHostRoomConnect object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self name:kOnConnect object:nil];
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:#"toSetListRoomVC" sender:self];
});
}
Upon getting this 'onHost' notification I perform the segue. Pushing to the VC like this works EVERY TIME. viewDidAppear gets called in SetListRoomVC ever time.
However, when pushing upon this notification. the destinationVC's viewDidAppear will NEVER get fired.I havnt made any changes to these methods in a while and I doubt it has to do with them, but just giving explanation as to what is happening.
-(void)receiveInitializeNotification:(NSNotification *)notificaiton
{
[[NSNotificationCenter defaultCenter]removeObserver:self name:kInitialize object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self name:kOnHostRoomConnect object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self name:kOnConnect object:nil];
if (self.joinLabelSelected) {
self.roomCodeTextField.inputAccessoryView.hidden = YES;
[self.roomCodeTextField resignFirstResponder];
self.menuView.hidden = YES;
[self returnJoinLabel];
[UIView animateWithDuration:.25 animations:^{
self.roomCodeView.alpha = 0;
self.blurEffectView.alpha = 0;
} completion:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:#"toSetListRoomVC" sender:self];
});
}];
}
}
I am also using a custom segue and a embedded navigation controller. This is how I have my custom segue set up.
#implementation CustomSegue
-(void) perform{
[[[self sourceViewController] navigationController] pushViewController:[self destinationViewController] animated:NO];
}
#end
All of my viewDidLoad,viewWillAppear,viewDidAppear implement [super view...:animated];
again, I haven't made any significant changes to any of my load,appear, methods in a while. I really don't understand why this is happening now.
Can anyone give an explanation for this? Also, every once in a while, maybe 10% of the time, the viewDidAppear method will get fired upon pushing the 'onHost' notification.
Thanks for any help. This boggling my mind.
EDIT: I rolled back to a commit from a few days ago and began rebuilding my project. So far viewDidAppear is working. I'll try to pinpoint what exactly is screwing everything up. Very strange though since most of what I have to rebuild is just Helper methods and minor UI updates.

These methods should be invoked every time and I've never seen them not being called.
Maybe you are using multiple subclasses inheriting from UIViewController and forgot to invoke super? In this case the super implementation is indeed not called:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// do your stuff
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// do your stuff
}
- (void)viewWillDisappear:(BOOL)animated {
// do your stuff
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
// do your stuff
[super viewDidDisappear:animated];
}
There might be some rare cases with interactive UINavigationController transitions, where a -viewWillDisappear: might get called, but no -viewDidDisappear:. E.g. if the user wipes from the left of the screen to navigate back and then cancels this action. This could result in a -viewWillDisappear:, but no corresponding -viewDidDisappear:.

Is your navigation controller ever behind anything? For example I think if you used presentViewController: to display a modal view controller on top of your navigation controller, and then pushed your VC onto the navigation controller, viewDidAppear wouldn't get called since iOS would think it was offscreen.
Have you tried putting NSLogs your navigation controller's viewWillAppear and viewWillDisappear methods to make sure that the navigation controller thinks it is visible when you push your new view controller?

Related

Dealing with the same NSNotification in multiple view controllers

I'm using the below notifications to reload ViewControllerA when my app comes back from background mode. It works correctly, but the applicationEnteredForeground: method gets called every time when I open the app again. For example if I close the app when ViewControllerB or ViewControllerC is on the screen and open it again the method will be called despite the viewDidLoad of ViewControllerB doesn't contain applicationEnteredForeground: method. I would like to know that how could solve this issue? My goal is to use applicationEnteredForeground: only when ViewControllerA was on the screen before I closed the app.
As a possible solution I would just remove the NSNotificationCenter in the viewDidDisappear, but since the observer is in the viewDidLoad it won't work when the user navigates back, because viewDidLoad won't be called again. Is there any fix for this?
- (void)viewDidLoad {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationEnteredForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
- (void)applicationEnteredForeground:(NSNotification *)notification {
// do stuff...
}
You should remove ViewController A's event listener on viewWillDisappear and add it in viewWillAppear. That way, VC A will only be listening when it is the visible view controller.
You can check if a view controller is on screen by checking the window property of it's view. It will work in most standard cases.
- (void)applicationEnteredForeground:(NSNotification *)notification
{
if (self.view.window == nil) {
// Not on screen
return;
}
// do stuff...
}

Reload all UIViewControllers in each UINavigationController of UITabBar

I have a tab based application and I want to update the information that is displayed in all UIViewControllers reloading them when I click in a button.
I have tried several ways to refesh the view controllers like [view setNeedsDisplay] and [view setNeedsLayout] but without success. I had also iterate all navigation controllers of tab bar and it's view controllers and applied this function calls without no success too.
Can anyone help me with this?
You basically add this to all controllers you want to refresh :
- (instancetype)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(refreshAllViews:) name:#"RefreshAllViews" object:nil];
}
return self;
}
- (void)refreshAllViews:(NSNotification *)notification
{
// Reload all the data and views you want here
// eg. [self.tableView reloadData];
}
- (void)reloadAllViewsAction:(id)sender
{
// Call this from the button to refresh all views
[[NSNotificationCenter defaultCenter] postNotificationName:#"RefreshAllViews" object:nil userInfo:nil];
}
Two options:
(1) Use notification, when user clicks the button, send a notification(Maybe with some informations), so that all the view controllers which are interested in that notification could update itself accordingly;
(2) Store the information in a shared model (Singleton) so it can be accessed by all the view controllers, and for each view controller, update UI in viewWillAppear according to that shared model

NSNotification is being called multiple times from UITabBarController

I have a UITabBarController, which has 4 tabs. Each one of those tabs is a separate UIViewController. I have objects on each one of those 4 VC's that use NSNotification's to perform actions upon the press of a certain object. The 4 VC's all respond to the notification in the same way because it is a similar object on each page. When this object is pressed it presents a view onto the current view controller. The problem is that if I move to any of the other 3 tabs now that view is also on their VC. That is because the notification is being responded to on all 4 tabs when it is pressed on any of the VC's. I am needing it to only respond to the VC that the user is currently on and not any of the others that are in the tab bar.
Is there a way to get this to work properly? Maybe a threshold where you can set how many times the notification can perform its selector after being called? That way I could set it to 1 and at any given time if that notification is called the selector can only be called 1 time.
The type of object implementation that I'm using requires me to use NSNotification's so there is no way to change how I interact.
edit:
This viewDidLoad method is on the top level VC for the 4 VC's in my tab bar. All 4 of them either use this directly or inherit from it.
- (void) viewDidLoad
{
...
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didSelectItemFromCollectionView:) name:#"didSelectItemFromCollectionView" object:nil];
}
Action Handler:
- (void) didSelectItemFromCollectionView:(NSNotification *)notification
{
NSDictionary *cellData = [notification object];
if (cellData)
{
NewVC *pushToVC = [self.storyboard instantiateViewControllerWithIdentifier:#"PushToVC"];
[self.navigationController pushViewController:pushToVC animated:YES];
}
}
Each of the 4 VC's is a UITableViewController and have cells with an object that can be pressed. This NSNotificationCenter action is what allows the operation to work.
You must have implemented the NSNotificationCenter's -addObserver:selector:name:object: method in the -viewDidLoad of every viewController
Example:
- (void)viewDidLoad
{
//...
[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doSomething:)
name:#"TestNotification"
object:nil];
}
Instead of having this in -viewDidLoad, move it within -viewWillAppear and implement removeObserver:name:object: in -viewWillDisappear.
This way, only the viewController that is currently on will respond to the notification.
Example:
- (void)viewWillAppear:(BOOL)animated
{
//...
[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doSomething:)
name:#"TestNotification"
object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
//...
[NSNotificationCenter defaultCenter] removeObserver:self
name:#"TestNotification"
object:nil];
}
- (void)doSomething:(NSNotification *)userInfo
{
//...
//if you push a viewController then the following is all you need
[self.navigationController pushViewController:vcSomething
animated:YES];
//however.... if you're instead presenting a viewController modally then
//you should implement "-removeObserver:name:object: in this method as well
//[NSNotificationCenter defaultCenter] removeObserver:self
// name:#"TestNotification"
// object:nil];
//[self presentViewController:vcSomething
// animated:YES
// completion:nil];
//OR... in the completion parameter as:
//[self presentViewController:vcSomething
// animated:YES
// completion:^{
// [NSNotificationCenter defaultCenter] removeObserver:self
// name:#"TestNotification"
// object:nil];
// }];
}
EDIT:
You (#Jonathan) commented:
I really appreciate your answer and it has helped me out a lot! I
actually ran into one more scenario where this issue occur's and I'm
not sure how to figure it out. Right now I have a VC that presents
another VC modally. Each one has observers for the same
NSNotification. Everything performs perfectly well when I'm in the
modally presented VC, but once I dismiss that VC and return to the
underlying one I have the same issue where the notification is being
called multiple times. Do you have an idea for a solution in this
case?
Now... regarding this...
FIRSTLY... NOTE:
Multiple -addObserver:selector:name:object: will register the specified notification multiple times (means... same notification being registered for N times will call invoke the target selector N times)
Presenting a ViewController (call it Child) from, say, Parent viewController will NOT invoke the -viewWillDisappear: of the Parent
where as...
Dismissing the Child viewController will still invoke -viewWillAppear: of the Parent
This creates an imbalance in the logic and if not handled (as per the commented lines in the code example of the doSomething method above), it results in the Parent registering for the notification multiple times (as it's -viewWillAppear: method is called more often than not -viewWillDisappear:)
also see: similar question
it should be called only once so that it never gets called again
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(methodName:) name:#"name" object:nil];
});

Present a Modal View When App Becomes Active

I want to present a modal view controller (for a login screen) when my app launches, and also when it becomes active again after a user has hit the home button and then relaunched the app.
I first tried to present the modal view in the root view controller's viewDidAppear: method. That works great when the app first launches, but this method isn't called when the app becomes active again.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self presentModalView];
}
- (void)presentModalView {
if(![AuthenticationService sharedInstance].isAuthenticated) {
_modalVC = [self.storyboard instantiateViewControllerWithIdentifier:self.modalViewControllerIdentifier];
_modalVC.delegate = self;
[self presentViewController:_modalVC animated:YES completion:nil];
}
}
Next I tried to call this from my app delegate in the applicationDidBecomeActive: method.
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
ModalPresentingUISplitViewController *splitViewController = (ModalPresentingUISplitViewController *)self.window.rootViewController;
[splitViewController presentModalView];
}
This appears to work fine on the surface, but I get a Unbalanced calls to begin/end appearance transitions for <ModalPresentingUISplitViewController: 0x7251590> warning in my log. I get the sense that I'm somehow presenting the modal view before the UISplitView is finished presenting itself, but I don't know how to get around this.
How can I "automatically" present a modal view from my root view controller when the app becomes active, and do it at the "right" moment as not to unbalance my split view controller?
Forgot this question was here. Yes, I have a solution. I can't help but feel that there is a more elegant or right way to do this, but this worked for me...
This assumes you are using ARC and storyboards; You've created a UIViewController for your login view with a modal segue from the UISplitViewController (or whatever your root view controller is).
UISplitViewController (or whatever your root view controller is)
- (id)initWithCoder:(NSCoder *)aDecoder {
if(self = [super initWithCoder:aDecoder]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(presentModalView) name:UIApplicationDidBecomeActiveNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.viewHasAppeared = YES;
[self presentModalView];
}
- (void) presentModalView {
if(self.viewHasAppeared && !self.userAuthenticated) {
[self performSegueWithIdentifier:#"ShowLoginView" sender:self];
}
}
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([[segue identifier] isEqualToString:#"ShowLoginView"]) {
JDPLoginViewController *dest = [segue destinationViewController];
dest.delegate = self;
}
}
- (void) dismissLogin {
self.userAuthenticated = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
Here are the important parts of the code to note...
We're calling presentModalView two places - in viewDidAppear which will take care of presenting our login view when the app first starts and
We are registering the presentModalView as an observer to the UIApplicationDidBecomeActiveNotification event so the method gets called when the app becomes active after being in the background.
Finally, we're creating a BOOL property viewHasAppeared on the UISplitViewController to keep track of whether the UISplitViewController's view has appeared or not so we don't try to present the modal login before the UISplitViewController's view has appeared.
Here are the different scenarios...
App First Starts:
presentModalView is called by the UIApplicationDidBecomeActiveNotification event, but since the UISplitViewController's view isn't loaded (and the viewHasAppeared BOOL is NO, nothing happens. Win. We don't present the view when we shouldn't.
Then eventually viewDidAppear is called, it sets viewHasAppeared to YES and then calls presentModalView. The login screen is presented. Everything works as expected - Yay!
App Becomes Active After Being in Background
presentModalView is called by the UIApplicationDidBecomeActiveNotification event again as in the first scenario, but this time viewHasAppeared is YES, so login view is presented as expected. Yay again!
Like I said, this feels kind of ugly, but it gets the job done until I find a better solution. Hope it works for you.
Have you tried UIView's viewWillAppear?
Chaining off #jpolete's answer I did things a little differently. In addition I wanted the login screen to only appear after the app has been in the background for more the 15 seconds (it's painful for the user to always have to log back in).
The source code for this demo can be found on github
Like #jpolete, I encapsulated most of the logic in the root view controller, which is a navigation controller in my case (iPhone example). The userLoggedIn flags whether or not the user has been authenticated. The presentingLoginController flag lets me know if the login screen is currently presented. backgroundTime holds a time stamp of when the user entered the background. Here is the class extension:
#interface RootNavigationController () <LoginDelegate>
#property (assign, nonatomic) BOOL userLoggedIn;
#property (strong, nonatomic) NSDate *backgroundTime;
#property (assign, nonatomic) BOOL presentingLoginController;
-(void)applicationDidBecomeActive:(NSNotification*) notification;
-(void)applicationDidEnterBackground:(NSNotification*) notification;
#end
When the view is loaded I add the appropriate notification hooks:
#implementation RootNavigationController
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Here we trigger the login segue if the user is not authenticated and we are not currently presenting the login controller.
-(void)loginIfNecessary {
if (!self.userLoggedIn && !self.presentingLoginController) {
self.presentingLoginController = YES;
[self performSegueWithIdentifier:#"RootLoginSegue" sender:self];
}
}
Here we set the root view controller to be the loginDelegate of the login controller.
This delegate is informed when a successful login occurs (the login controller is embedded in another nav controller):
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"RootLoginSegue"]) {
UINavigationController *navController = segue.destinationViewController;
LoginTableViewController *loginController = (LoginTableViewController *) navController.topViewController;
loginController.loginDelegate = self;
}
}
When a successful login occurs we do the following:
-(void)didLogin { // LoginDelegate method called to login controller after successsful login
self.presentingLoginController = NO;
self.userLoggedIn = YES;
}
When the view appears for the first time, when it appears after being in the background, or after being "covered up" we login (if needed):
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self loginIfNecessary];
}
When we enter the background we record the time:
-(void)applicationDidEnterBackground:(NSNotification*) notification {
self.backgroundTime = [NSDate date];
}
When we enter the foreground and its the first time or sufficient time has passed then we
force the user to log in again (if necessary):
-(void) applicationDidBecomeActive:(NSNotification*) notification {
const NSTimeInterval maxBackgroundTime = 15.0;
if (!self.backgroundTime || [[NSDate date] timeIntervalSinceDate:self.backgroundTime] > maxBackgroundTime) {
self.userLoggedIn = NO;
}
[self loginIfNecessary];
}
#end

removeObserver with NSNotification... what am I doing wrong?

Basically, I have a view1 which at some point, calls view2 (via presentModalViewController:animated:). When a certain UIButton in view2 is pressed, view2 is calls a notification method in view1 and immediately afterward is dismissed. The notification method pops up an alert.
The notification method works fine and is called appropriately. The problem is, every time view1 is created (only one view1 should exist at a time), I presumably get another NSNotification being created because if I go from view0 (the menu) to view1, then back and forth a few times, I get a series of the same alert message, one after another, from the notification method as many times as I opened a view1.
Here is my code, please tell me what I'm doing wrong:
View1.m
-(void) viewDidLoad {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(showAlert:)
name:#"alert"
object:nil];
}
-(void) showAlert:(NSNotification*)notification {
// (I've also tried to swap the removeObserver method from dealloc
// to here, but it still fails to remove the observer.)
// < UIAlertView code to pop up a message here. >
}
-(void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
View2.m
-(IBAction) buttonWasTapped {
[[NSNotificationCenter defaultCenter] postNotificationName:#"alert"
object:nil];
[self dismissModalViewControllerAnimated:YES];
}
-(void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
Calling -dealloc doesn't automatically happen after the view controller is dismissed — there can still be some "life" left in the view controller's lifetime. In that timeframe, that view controller is still subscribed for that notification.
If you remove the observer in -viewWillDisappear: or -viewDidDisappear:, this will have a more immediate effect:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:#"alert"
object:nil];
}
If you implement the removal of Observer in the viewWillDisappear: or viewDidDisappear: then you should not leave the addition of the observer in the viewDidLoad.
Instead put the addition of the observer in the viewWillAppear:. The problem you are having is because when any view is shown onto of the UIViewController view the removal of your observer will occur and since you added observer in viewDidLoad which will happen only once, it will be lost.
Keep in mind that this approach works well for objects you do not wish to observer while your main view is not in the fore front.
Also Keep in mind that viewDidUnload has been depreciated too.
There is nothing wrong putting removeObserver: in dealloc. Just the fact it's not called means view1 not properly releases after dismissing. Looks like something holds pointer to your view1, check for retain cycles.
Also, you shouldn't call dealloc on super.

Resources