I am writing a turn-based game for the iOS platform. The client communicates with a remote server using the CocoaAsyncSocket API. Right now I work on this case: the client has been inactive for a while, and has been disconnected from the server due to timeout. If that's the case, I wish to pop back to the login view when the app enters the foreground, to let the user log back in again.
I assume I have to do this kind of work in the -(void)applicationWillEnterForeground of my app's delegate. Checking whether I'm connected or not is not a problem, but I don't know how to dismiss every presenting view controller to take me back to the root view (which happens to be the login view).
My view controllers are presented modally, but I also have two navigation controllers with table views including push segues.
Any help with this problem is highly appreciated. Thanks in advance!
The jarring way to do it is just replace the rootViewController with a new login view controller.
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([self isDisconnected]) {
self.window.rootViewController = [MyLoginController new];
}
}
For storyboards, assuming your initial storyboard is the login storyboard.
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([self isDisconnected]) {
UIStoryboard *storyboard = self.window.rootViewController.storyboard
self.window.rootViewController = [storyboard instantiateInitialViewController];
}
}
Depending on your UX, this may or may not be appropriate.
If all of your content views are presented modally from the root view controller (which doesn't sound like a great idea) then from the app delegate (indeed in applicationWillEnterForeground),
// if we need to login
UIViewController *rootViewController = self.window.rootViewController;
[rootViewController dismissViewControllerAnimated:YES completion:nil];
If the root view controller isn't presenting modally then you need to dismiss in the appropriate way. If any other 'child' view controller is presenting modally then it should dismiss. I guess dismissing when that view controller changes parent view controller will work. You may need to add a notification to inform all view controllers that everything is being torn down.
Related
I am trying to change root controller on app delegate.
I am making a server call to check if login session is valid or not. If valid, show welcome page, else show login page.
In the appDelegate , I am making server call in the method didFinishLaunchingWithOptions.
I get response through my datamodel delegate, but by the time I get response from server, the old root controller is already presented on screen.
At present, old root controller is presented first and within a fraction of seconds, new root controller is presented! So it looks like a flickering thing!
Is there a way to hold the old root controller to present untill I get repsonse from server ?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
_dataModel=[DataModal sharedInstance];
_dataModel.delegate=self;
NSString *token=[[NSUserDefaults standardUserDefaults]objectForKey:#"token"];
if (!token) {
token=#"jkfhgjkfg908";
}else{
_dataModel.auth_token=token;
}
[_dataModel checkToken:token];
return YES;
}
//By the time it reaches here, the old root controller is already present on screen
-(void)checkToken:(NSDictionary *)items{
if ([items[#"success"] isEqual:#1]) {
AppDelegate *app = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIStoryboard *storyboard=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
app.window.rootViewController=[storyboard instantiateViewControllerWithIdentifier:#"DashboardNavController"];
}else{
//do nothing, defualt is login view
}
}
You should not be waiting on a server call to decide on your root view controller - once didFinishLaunchingWithOptions is called you need to be presenting a view.
Have one Root View Controller present an initial view controller on load and stick to it - it's root for a reason.
If you have a token cached, skip your loginViewController and segue on. Plan for no internet/poor connection/ expired tokens, but don't put asynchronous actions in app delegate that prevent your UI being presented.
Uncheck Is initial View Controller checkbox for a controller in Main.storyboard
Initialize appDelegate's window property: appDelegate.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
Set rootViewController: appDelegate.window.rootViewController = controller;
And call makeKeyAndVisible method: [appDelegate.window makeKeyAndVisible];
Rather than changing rootViewController, using initialViewController as an entrance is a better approach in my opinion. When app is launched there must be a screen to make the user feel everything is going well. Also, you are making a request to server and this process may fail or timed out.
My advice is to make navigation through initialViewController. Make it visible and put some animation or activity indicator and then when the response come, if logged in, go to your apps mainViewController, else go to loginViewController. Also you can check internet connection here and advice user to connect to internet.
You cannot hold the old view controller to present, but instead you can present a view controller with black screen and then after you got the response form the server about the session you could present the view controller based on your needs.
I have an app where I re-init the app if it's been in the background for too long. When the user opens up the app after the allotted time, the re-init happens, and I display the splash screen while I get the data I need. When I get the data from the server I set the window.rootViewController property to a new value. If the old root view controller has had a view controller presented modally, and that view controller was being displayed when the re-init happens, dealloc doesn't get called on the view controller (I've tested this by putting NSLog's in the dealloc method). In the case where a modal view controller was not presented, the dealloc gets called as expected.
Does anyone know of a solution to this? I'm not sure if it's an Apple bug, or if it's something that I need to handle on my own.
The solution that I came up with was before I set the RootViewController, I call
- (void)_dismissRootViewControllersModalViewsIfAny {
UIViewController *rootViewController = self.window.rootViewController;
if (rootViewController.presentedViewController || rootViewController.presentingViewController)
{
[rootViewController dismissViewControllerAnimated:NO completion:nil];
}
}
This is a method that I created, which makes sure that if there is a modal view controller to dismiss, it will be dismissed.
Your modal view is presented by the rootViewController (it's presentingViewController property is set to the rootViewController), that might be the source of your problem.
You can set your rootViewController to an instance of UINavigationController and then just use its setViewControllers:animated: method to display a freshly instantiated view controller instead of switching the window's rootViewController.
Currently I have two flows laid in storyboard: the onboarding / login flow (currently set as the initial view controller in storyboard settings) and the main application flow (e.g. the core application experience after you're logged in).
The transition from onboarding to the main app flow happens successfully in the UI, but I don't think the way I'm doing it is quite right. One problem I'm having is the inability to restore view state to the main application flow.
When transitioning from the onboarding flow to the main app flow, I'm using presentViewController to transition to the new view controller, and then I'm removing the presenting view controller's parent navigation controller from it's view hierarchy using removeFromParentViewController. My idea here is that the main app flow should really have it's own view hierarchy, the root of which should be the first view in the newly presented view controller.
However, doing things in this manner breaks the view restoration code. It seems I'm somehow failing to properly "reset" the view hierarchy to the main navigation controller. View state restoration was working before when I was not removing the onboarding navigation controller from the view hierarchy (as in the code below), but now it's not working -- state is always restored back to the beginning of the onboarding flow, the - (void)decodeRestorableStateWithCoder:(NSCoder *)coder method is never called on the main app flow controller.
Below is the relevant code from the onboarding flow controller that presents the main app flow controller.
// The presenting view controller from the onboarding flow
#implementation TLOnboardingStep2ViewController
-(void)presentMainViewController {
UIStoryboard *board = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
// The root view controller of the main app flow
ECSlidingViewController *slidingVC = [board instantiateViewControllerWithIdentifier:#"slidingViewController"];
[self presentViewController:slidingVC animated:YES completion:^{
[[UIApplication sharedApplication].delegate window].rootViewController = slidingVC;
[self.navigationController removeFromParentViewController];
}];
}
#end
What am I doing wrong here? Thanks.
It seems like a better way to handle this is to make the main application flow the root view controller, and pop into the login flow as a modal view controller from the main view controller when needed. This is what I'm doing now and it seems to be working a lot better, and keeping the hierarchy much cleaner.
Alright, so I've been looking around for an answer to the following question: How to push a certain view controller into view when a push notification arrives from the AppDelegate?
And most answers are that if the rootViewController is a UINavigationController, I have to instantiate a view via my StoryBoard and push it with that root navigation controller.
Here's my situation. Here's how my storyboard is organised:
So as you see my rootViewController doesn't have UINavigationController. So, how do I go about pushing a certain view out of my storyboard?
NOTE: Presenting some separate modal view for push notifications isn't really a great idea. It's my last resort.
I'd like a solution as in the Apple Mail and Message apps.
Alright, I guess I don't give up that easily :) Here's the solution I found to my problem. Since I am presenting my main app views from within a modal view controller, here's what I did:
Since iOS 5 every view controller has a presentedViewController property. Once you know that, it's pretty easy from there.
Here's some specific code from inside AppDelegate.m
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
if ([[userInfo objectForKey:#"notificationType"] isEqualToString: #"messageType"]) {
UITabBarController * tabBarController = (UITabBarController *)[self.window.rootViewController presentedViewController];
[tabBarController setSelectedIndex:kChatViewIndex];
// My view controller that I presented modally is a tabBar, your case can be different.
// ... so from here I can reach any navigation controller or any other view from inside my app
}
Now since you've got your view controller, you can use setSelectedIndex: if it's a tabBarController, or push a certain view controller if it's a navigation unit.
Hope this helps anyone with a similar problem.
Cheers!
I am coding a game where the user can only play if there is an active internet connection. I want to dismiss all ViewControllers and get back to the login screen if the internet status changes. Here's the hierarchy:
My App Delegate show the LoginViewController by:
self.window.rootViewController = loginViewController;
[self.window makeKeyAndVisible];
Here the user logs in. While logging in I check for connectivity and handle errors. If login is successful loginViewController presents mainViewController using
[self presentModalViewController:self.mainViewController animated:YES];
Then mainViewController presents other controllers, which can present other controllers and so on. The present and dismiss routine is working properly.
Now, if Reachability status changes, I want to display an alert and force the app to get back to the login screen. I know how to listen for these notification and take actions appropriately. It is well documented :)
How do I dismiss all view controllers until I land back in the login screen? Do I need to listen to the notification in all view controllers and dismiss them separately? Is there any way of catching the notification in a parent (like the app delegate) and dismissing all view controllers from there (without holding a reference to all of them beforehand)?
Obviously, I'm not using a navigationController so
[self.navigationController popToRootViewControllerAnimated:NO];
is not possible.
Edit: Alternately, I can dismiss and release everything and start from scratch, like if the application is started new. Is this a better approach? How exactly do I do it?
Thank you.
OK I solved it. Simply calling
[self dismissModalViewControllerAnimated:NO];
on the loginViewController dismisses main view controller and all controllers above on the stack.