Root View Controller's view not in the Window Hierarchy - ios

I have coded a login page which in my app delegate (under the method application:didFinishLaunchingWithOptions:) is set to be my root view controller with
[self.window setRootViewController:vc];
Where vc is the instantiation of my HomeVC.
This view loads correctly. I can enter in all of the data required for login just fine but as soon as I try and load another view I get
Warning: Attempt to present <RegisterVC: 0x7d07f520> on
<HomeVC: 0x7d374830> whose view is not in the window hierarchy!
I tried a little hack to be certain I got the top most view controller using the following code:
- (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
but, as I suspected, it only ever returned the current view controller, which is also the root view controller. It never even enters the while loop.
I cannot understand how the view controller I'm trying to call from is not on the hierarchy when it's not only displaying correctly but is also the only view on the hierarchy at all.
Edit 1: I'll include the AppDelegate application:didFinishLaunchingWithOptions: method's section where I create and add the VC and the Navigation VC.
UIViewController *vc = (HomeVC *)[[HomeVC alloc] initWithNibName:NSStringFromClass([HomeVC class]) bundle:nil];
self.navigationController = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController setNavigationBarHidden:YES];
[self.window setRootViewController:vc];
//^^^This is the line I'm most suspicious of, is that correct?^^^
[self.window setBackgroundColor:[UIColor colorWithHexString:kDefaultBackgroundColor]];
[self.window makeKeyAndVisible];

you can get the root view controller of the window, which should be the navigation controller, and then get its top view controller.
try like this:if you are sure there is a viewController.presentedViewController then send includeModal as YES.
- (UIViewController *)topmostViewControllerFrom:(UIViewController *)viewController
includeModal:(BOOL)includeModal
{
if ([viewController respondsToSelector:#selector(selectedViewController)])
return [self topmostViewControllerFrom:[(id)viewController selectedViewController]
includeModal:includeModal];
if (includeModal && viewController.presentedViewController)
return [self topmostViewControllerFrom:viewController.presentedViewController
includeModal:includeModal];
if ([viewController respondsToSelector:#selector(topViewController)])
return [self topmostViewControllerFrom:[(id)viewController topViewController]
includeModal:includeModal];
return viewController;
}

Related

How to return to root ViewController, if the current is deeply nested?

I have a problem that my iOS app has several ViewControllers.
For example: ViewControllers named like A, B, C. A jumped to B with pushViewController, B jumped to C with presentViewController , C jumped to D with presentViewController and so on.
If the current ViewController is Z or some other ViewController, how can I jump back to A directly?
Try Below code. It will work for all cases :
for (UIViewController *controller in self.navigationController.viewControllers)
{
if ([controller isKindOfClass:[nameOfYourViewControllerYouWantToNavigate class]])
{
if(controller.isBeingPresented)
[controller.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
else
[self.navigationController popToViewController:controller animated:YES];
break;
}
}
[[self navigationController] popToRootViewControllerAnimated:YES];
It will pop your viewcontroller to Root controller.
For the view controllers show by pushViewController, you can get rootViewController via [self.navigationController.viewControllers objectAtIndex:0]
Others shown by presentViewController, you can get parent view controller via self.presentingViewController
Whatever you want to go back to A ViewController from any other view controller, first you have to set that as RootViewController. If you use XIB you must set as root in app delegate didFinishLaunchWithOptions method. If you use storyboard you should set NavigationController in storyboard and set AViewController as ROOT using control+drag(mouse).
I work in a project which has lot of view controllers. It has pushViewController and PresentViewControllers also I set A asRootViewController. So I can return from any view controller to RootViewController.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
self.window = [[UIWindow alloc]initWithFrame:[[UIScreen mainScreen] bounds]];
AViewController *aVC = [[AViewController alloc]initWithNibName:#“AViewController" bundle:nil];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:aVC];
self.window.rootViewController = aVC;
[navController setNavigationBarHidden:YES];
self.window.backgroundColor = [UIColor clearColor];
[self.window makeKeyAndVisible];
return YES;
}
For going to AViewController from any other view controller
- (IBAction)actionGoBack:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}

iOS custom UIViewController container - view lifecycle and dealloc

I have written a custom view controller container that can replace the existing view controller (VC1) with a new view controller(VC2) i.e. a replace type segue rather than pop/push.
Everything worked fine until testing with iOS 8. It seems a second party I'm using causes a crash if the view controller is replaced before the previous view controller is dealloc
i.e. if [VC2 viewDidLoad] called before [VC1 dealloc].
It is my understanding that I have no control over the dealloc so this kind of behaviour is out of my control. However, I want to make sure that I am not causing this behaviour in the way I'm controlling my child view controllers.
The following is pseudo code for my view controller container:
- (void)replaceStackWithNewController:(UIViewController*)newVC {
for(UIViewController *vc in [self viewControllers]) {
[vc willMoveToParentViewController:nil];
if([vc isViewLoaded]
[[vc view] removeFromSuperview];
}
[vc removeFromParentViewController];
}
//self.viewControllers is my navigation stack
self.viewControllers = nil;
self.childVC = nil;
if(newVC != nil) {
self.childVC = newVC;
}
[self addChildViewController:newVC];
//this adds the view to a pre existing wrapper
[self addView:newVC.view ToWrapper:wrapper];
//add controller to stack
[self.viewControllers addObject:newVC];
[newVC didMoveToParentViewController:self];
}

Presenting Login as modal iOS?

I have an app that has a login nav controller and a tab bar controller. I have set my tab bar controller to the root controller however I want the login navigation controller to display as a modal so that I can dismiss it when they're logged in and not show it at all if they are. It is reading the right line but failing to present the landingviewcontroller. When I run the app it jumps straight to the TabBarController.
My code is as follows:
I have a method which checks if you are logged in in my app delegate which is where I am telling it to present the landing view controller (login). I know from stepping through that it is correctly determining that I'm not logged in and going to this line of code on running:
[self.window.rootViewController presentViewController:landingVC animated:YES completion:nil];
The full app delegate:
#import "GFAppDelegate.h"
#import "GFCredentialStore.h"
#implementation GFAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:[NSBundle mainBundle]];
UIViewController *tabBarController = [storyboard instantiateInitialViewController];
UIViewController *landingVC = [storyboard instantiateViewControllerWithIdentifier:#"LandingViewController"];
GFCredentialStore *store = [[GFCredentialStore alloc] init];
if (store.isLoggedIn) {
self.window.rootViewController = tabBarController;
} else {
[self.window.rootViewController presentViewController:landingVC animated:YES completion:nil];
}
// Set root view controller and make windows visible
[self.window makeKeyAndVisible];
return YES;
}
I've tried to make this clear, but understand it's probably confusing as poorly written. Thanks for any help.
What you need to do is to always set rootViewController to tabBarController, but if user is not logged call presentViewController from it. Something like it:
self.window.rootViewController = tabBarController;
[self.window makeKeyAndVisible];
if (!store.isLoggedIn) {
[tabBarController presentViewController:landingVC animated:YES completion:nil];
}
Try this:
self.window.rootViewController = tabBarController;
[self.window makeKeyAndVisible];
if (store.isLoggedIn==false) {
[tabBarController presentViewController:landingVC animated:YES completion:nil];
}
Your problem is that when tries to present view controller from self.window.rootViewController is does not exist so rootViewController == nil.
I suggest you to not present it as modal (since you don't have controller to present from), but to set login view controller as root.
self.window.rootViewController = landingVC;
But if your intent was to present login above tab bar see answers suggested before mine.

iOS State restoration and UINavigationController modal views

I am trying to incorporate State Restoration in my app. I have it working fine for the most part, but presenting a navigation controller for a modal view on top of another navigation controller seems challenging.
For testing, I created a new split-view app on the iPad, with navigation controllers for both sides of the split view, and a Master and Detail view controller for each side, the roots of their respective navcontrollers. In the master view, you can click on a button to push a new TestViewController onto the navController stack programatically. I hook up the splitView in the storyboard, add restorationIDs to everything, opt-in to the delegate, provide a restoration class and adhere to the UIViewControllerRestoration protocol for TestViewController (since it's created programmatically) and everything works fine. If I close the app and retort it, it will start the TestViewController pushed onto the master's navcontroller. So far so good.
I then change the button handler to present the TestViewController inside a new UINavigationController, present it onto the master's navigation controller, to show a modal view (instead of pushing it on the nav stack). Now, when I relaunch the app, there is no modal view there anymore. TestModalViewController's viewControllerWithRestorationIdentifierPath:coder: is actually called correctly as before, but the modal view is never presented for some reason.
Here is the code for what I'm talking about
MasterViewController.h:
- (void)pushButton:(id)sender
{
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationIdentifier = #"testid";
test.restorationClass = [TestModalViewController class];
UINavigationController *modal = [[UINavigationController alloc] initWithRootViewController:test];
modal.modalPresentationStyle = UIModalPresentationFormSheet;
modal.restorationIdentifier = #"ModalTestID";
[self.navigationController presentViewController:modal animated:YES completion:nil];
return;
}
TestModalViewController.m:
+ (UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationClass = [TestModalViewController class];
test.restorationIdentifier = [identifierComponents lastObject];
return test;
}
Perhaps the UINavigationController that is created to display modally is never preserved? Not sure why, because it does have a restorationIdentifier.
Edit:
After further testing, it turns out if I remove the UINavigationController from the the pushButton: code, and present the TestModalViewController instance directly, it gets restored correctly. So something about the UINavigationController being presented from another UINavigationController?
This works (though not what I really want):
- (void)pushButton:(id)sender
{
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationIdentifier = #"testid";
test.restorationClass = [TestModalViewController class];
//UINavigationController *modal = [[UINavigationController alloc] initWithRootViewController:test];
//modal.modalPresentationStyle = UIModalPresentationFormSheet;
//modal.restorationIdentifier = #"ModalTestID";
[self.navigationController presentViewController:test animated:YES completion:nil];
return;
}
EDIT:
Attached link to test project: dropbox.com/sh/w8herpy2djjl1kw/vw_ZWqimgt
It's basically the Core Data master-detail template; run it on the iPad simulator. The + button in Master invokes the TestModalVC; if you then press the Home button, then kill debugger and launch again, you see the snapshot contains the TestModalVC but when the app is launched, it doesn't get restored
You can either create your own restoration class to handle this, or add the following to your app delegate:
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
coder:(NSCoder *)coder
{
NSString *lastIdentifier = [identifierComponents lastObject];
if ([lastIdentifier isEqualToString:#"ModalTestID"])
{
UINavigationController *nc = [[UINavigationController alloc] init];
nc.restorationIdentifier = #"ModalTestID";
return nc;
}
else if(...) //Other navigation controllers
{
}
return nil;
}
More information in the documentation.

Instantiate UINavigationController on button action

I have simple application with only one main view, which has 'Settings' button, and settings are tree-grouped, so I wand to present them in navigation controller. And I don't want navigationController in main view, because I don't want navigation bar there.
That's why I don't instantiate navigationController in application: didFinishLaunchingWithOptions:. And when I check self.navigationController in 'Settings' button handler, it returns nil.
So I wrote this: (I use ARC)
- (void)doSettings
{
NSLog(#"%#", self.navigationController); // prints nothing
SettingsViewController *settingsViewController = [SettingsViewController alloc] initWithStyle:UITableViewStyleGrouped];
UINavigationController *navigationController = [[UINavigationController alloc] init];
[self.view.window setRootViewController:navigationController];
[navigationController pushViewController:settingsViewController animated:YES];
}
This works, although it pushes settingsViewController without animation (don't know why).
Is this generally the correct way to do - to change rootViewController in the middle of running app?
And if yes - than when I'm done with Settings, I probably need to set rootViewController back to current viewController, as it was before I tapped 'Settings'?
I think you want to create a navigation controller that you will present modally; the following will do:
SettingsViewController* settingsViewController = [[SettingsViewController alloc] initWithStyle:UITableViewStyleGrouped];
UINavigationController *navigationController = [[UINavigationController alloc] init];
[navigationController pushViewController:settingsViewController animated:YES];
[self presentViewController: navigationController animated: YES completion:nil];
where self here is the view controller you want to trigger the modal view controller from.
since you present modally the navigation controller you can dismiss it within the code source of your settingsViewController by accessing its navigation controller:
[self.navigationController dismissViewControllerAnimated:YES completion: nil];
To answer your question setting the rootViewController is not the correct way. Present the new vc modally through the presentViewController method.
A better way is to build the navigation vc and present it over your main vc (not replace your main vc).
- (void)doSettings
{
NSLog(#"%#", self.navigationController); // prints nothing
SettingsViewController *settingsViewController = [SettingsViewController alloc] initWithStyle:UITableViewStyleGrouped];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController: settingsViewController];
[self presentViewController: navigationController animated:YES completion:^{}];
}
Your main vc might realize (maybe as a delegate) that the settings flow is complete. It can then dismiss the presented navigation controller with:
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
Alternatively, the setting flow could dismiss itself...
// somewhere in the settings vc or a vc it pushes, when we decide settings are done
self.navigationController dismissViewControllerAnimated:YES completion:^{}];
Have navigation in main view and have below line (which will hide navigation bar)
[[self navigationController] setNavigationBarHidden:YES animated:YES];
(I would say have this in viewWillAppear and viewDidLoad both, BUT in viewWillAppear is MUST).
Now in second view, to show navigation bar
[[self navigationController] setNavigationBarHidden:NO animated:YES];
Hope this will solve your problem...

Resources