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
Related
Ok, so I'm working on an objective-c ios app here where I have multiple portrait view controllers. I don't however want any of these view controllers to display in landscape, instead what I want to happen is for a single landscape view controller to segue in when the device is rotated to landscape and segue out again to a specific view controller when rotated back to portrait. This isn't just a case of resizing an exisiting view, how do I go about doing this?
I haven't tried this, but I'd start this way: Listen for orientation changes...
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
Upon change to landscape, present a landscape vc. Upon change to portrait, dismiss it...
- (void)orientationDidChange:(NSNotification *)notification {
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if (UIInterfaceOrientationIsPortrait(orientation) && self.presentedViewController) {
[self.presentedViewController dismissViewControllerAnimated:YES completion:nil];
} else if (UIInterfaceOrientationIsLandscape(orientation) && !self.presentedViewController) {
MyLandscapeVC *landscapeVC = [[MyLandscapeVC alloc] initWithNibName:#"MyLandscapeVC" bundle:nil]; // or however you make this
[self presentViewController:landscapeVC animated:YES completion:nil];
}
}
The extra check for self.presentedViewController is to guard against any sloppiness in the system during quick rotations back and forth, so we never get more than one of the landscape vcs stacked up. It may be unnecessary.
This can be isolated in one file using a class category on UIViewController, like this...
// in UIViewController+Rotations.h
#interface UIViewController (Rotations)
- (void)observeRotations;
#end
// in UIViewController+Rotations.m
#import "UIViewController+Rotations.h"
#implementation UIViewController (Rotations)
- (void)observeRotations {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
}
- (void)orientationDidChange:(NSNotification *)notification {
// etc., from above
}
#end
Just import UIViewController+Rotations.h in your view controllers, and early in the lifecycle (probably viewWillAppear) call [self observeRotations];
I am newbie working on objective C. I am facing one problem .
I have tab bar controller containing three view controllers out of which I am concerned about only two VCs named "Setting" and "BBVC" . "BBVC" has a UIButton and "Setting" has a UISwitch (Please see below image).
When button "B" is pressed, in tab bar view controller below code gets executed :
- (void)centerButtonTapped:(id __unused)sender {
BBVC *vc = [[BBVC alloc] init];
UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];
[self presentViewController:nc animated:YES completion:nil];
}
BBVC gets loaded as pop UP
My aim is I want to change the value of "UISwitch" based on "UIButton" action event.
Case 1 : Not On setting View
In this case after pressing the UIButton, when I am on
"Setting" VC, the aim can be achieved by using viewWillappear and UserDefault as shown below :
- (void)viewWillAppear:(BOOL)animated
{
NSLog(#"viewWillAppear");
[super viewWillAppear:animated];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[Switch setOn:[defaults boolForKey:#"EnableLIVE"] animated:YES];
}
Case 2 :
In this case I am already on "Settings" VC (i.e. setting view is already loaded) and when button "B" from tab bar is pressed, it gets loaded as a pop up as shown in below image. I am trying to achieve my aim but its not working.
Attempt 1 :
In Setting VC, I updated the code in "viewDidAppear" method but while debugging I got to know after closing BBVC, method "viewDidAppear" is not getting called.
-(void)viewDidDisappear:(BOOL)animated
{
NSLog(#"viewDidDisappear");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[Switch setOn:[defaults boolForKey:#"EnableLIVE"] animated:YES];
}
Attempt 2 :
Use Delegate and Protocols :
I have used delegate and protocols which is working fine but in this case address of UISwitch is nil. Please see below image
Note : UISwitch is created programmatically.
I am clueless here. Any kind of help is appreciated.
Thank you.
If i'm interpreting your question correctly, it sounds like the main problem you're experiencing currently is updating the live switch on the settings VC when it's already displaying, but the BBVC is displaying modally overtop (and it's button is pressed).
You can listen for a notification of changes to user defaults within your settings controller when it loads up, and remove it as an observer once deallocated -- then adjust the switch to the appropriate value once the notification of user defaults changing comes in. Something along these lines:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.liveSwitch setOn:[[NSUserDefaults standardUserDefaults] boolForKey:#"EnableLIVE"]];
}
- (void)userDefaultsDidChange:(NSNotification *)notification {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self.liveSwitch setOn:[[notification object] boolForKey:#"EnableLIVE"]];
}];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
https://developer.apple.com/documentation/foundation/nsuserdefaultsdidchangenotification?language=objc
Scenario:
I need to show 3 or more popups one after the other on button click in each popup. I have created a different viewcontroller and xib files for each popup. So for displaying each popup I have used presentViewController instead of pushViewController.
That is, I have used this:
[self presentPopupViewController:searchPopUpView animationType:0];
instead of
[self.navigationController pushViewController:searchPopUpView animated:YES];
For dismissing a popup, the following code has been written:
[self dismissPopupViewControllerWithanimationType:0];
Issue:
The popups are displaying perfectly, but the background gets darker and darker whenever a popup shows up. After all popups have been dismissed I have to finally click on the blank screen to remove those darker parts. How to overcome this issue?
I think you are using MJPopupViewController to show pop-up.
If it is so, Then try this.
Suppose there is a controllerA from which you want to show a pop-up controller popupControllerB.
Then in your controllerA add Notifications Observer
Code to write in controllerA :
// Add Notification Observer when your view initialise.
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(dismissPopup) name:#"DISMISS_POPUP" object:nil];
In viewWillDisappear remove the notifications observer
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
This method will be called when you Post-notification from your popupControllerB
-(void)dismissPopup {
[self dismissPopupViewControllerWithanimationType:MJPopupViewAnimationFade];
}
And In popupControllerB, Where you want to dismiss the Pop-up, write this code.
[[NSNotificationCenter defaultCenter] postNotificationName:#"DISMISS_POPUP" object:nil];
Above line of code will call a method written in your controllerA and dismiss the pop-up properly.
If you want to dismiss presented UIViewControllers you can use this code. I have used this approach to dismiss presentedViewControllers. It will dismiss all your presentedViewControllers on your rootViewController.
UIViewController* presVC = self.window.rootViewController;
while (presVC) {
UIViewController* temp = vc.presentingViewController;
if (!temp.presentedViewController) {
[vc dismissViewControllerAnimated:NO completion:^{}];
break;
}
vc = temp;
}
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...
}
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?