presentViewController Warning view is not in the window hierarchy - ios

I am loading LaunchViewController as the root view controller in my AppDelegate's application:didFinishLaunching method:
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"LaunchViewController" bundle:nil];
LaunchViewController *launchViewController = [storyboard instantiateInitialViewController];
launchViewController.managedObjectContext = [CurrentSession mainQueueContext];
self.window.rootViewController = launchViewController;
[self.window makeKeyAndVisible];
LaunchViewController communicates with a web server to fetch some data. I am using AFNetworking library for asynchronous communication with the web server. In the success callback after fetching the data, I am presenting the LoginViewController. My understanding is that the callbacks in case of AFNetworking are performed on the main thread. Nevertheless I used performSelectionOnMainThread just to see if that would resolve the issue:
Inside callback:
[self performSelectorOnMainThread:#selector(presentLoginView) withObject:nil waitUntilDone:NO];
presentLoginViewmethod:
- (void)presentLoginView {
LoginViewController *loginViewController = [[LoginViewController alloc] initWithNibName:#"LoginViewController" bundle:nil];
loginViewController.managedObjectContext = self.managedObjectContext;
[self presentViewController:loginViewController animated:YES completion:nil];
}
The login view is not presented and in the console I see following message:
Warning: Attempt to present <LoginViewController: 0x78e47c30> on <LaunchViewController: 0x78eb4a60> whose view is not in the window hierarchy!
:-(
UPDATE
I put a breakpoint in viewDidLoad, viewDidUnload, viewWillAppear and viewDidAppear methods of the LoginViewController. viewDidLoad was hit. Others were not.
Why would the view get loaded but not appear?

That's probably happening because the AFNetworking execute the callback very fast, for instance before the view of the view controller is loaded, maybe because you are using a local server or another configuration in viewDidLoad that is slow. In this case I would recommend to move the code to viewWillAppear or viewDidAppear since in that moment the view would be in the hierarchy and therefore you would be able to present another ViewController from there.

In my AFNetworking success callback I was setting some of the information obtained from the web server into NSUserDefaults. And then I was presenting the LoginViewController.
My AppDelegate was listening to NSUserDefaultsDidChangeNotification notification. The handler for this notification would set the launchViewController as the root view controller.
So, in the AFNetworking callback while I was asking LaunchViewController to present LoginViewController I was also sending off NSUserDefaultsDidChangeNotification which would cause the AppDelegate to load the LaunchViewController.
I never saw even a flicker of LoginViewController ever. So, I think the AppDelegate handler was getting fired first and then the presentLoginViewController was getting called. And this somehow lead to the views not being added to window hierarchy message.

Related

PresentViewController completion block called too soon

I am modally presenting a ViewController from another one. The completion block is being called immediately, while the presented VC is still presented. Why would that be? Code follows.
UIStoryboard* sb = [UIStoryboard storyboardWithName:...];
UINavigationController* nc = [sb instantiateViewController...];
nc.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:nc
animated:YES
completion:^{ /* Called immediately! */ }];
There is stuff I want to do only when the presented VC is finished. I have a workaround but my understanding is I should be able to do it in the completion block.
The completion block is called once the viewController has been presented, as per Apple's documentation. This is the intended behavior: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/presentViewController:animated:completion:
I'd suggest wiring up some sort of callback in your viewController's viewWillDissappear to perform an action if necessary.
OK I had RTFM and saw this: "completion: The block to execute after the presentation finishes." Which really does imply what I thought. Ambiguous at best.
HOWEVER... elsewhere we read "The completion handler is called after the viewDidAppear: method is called on the presented view controller." Which is quite another thing, and confirms Ian.
So my workaround is actually the right way to do it...

How do I present a ViewController over another with that VC having no knowledge of the new one?

I had a method to do this, but it stopped working at some point.
The motivation here is for debugging. I have a button that shows a debugging action sheet from whatever VC calls it. This works great. However, in the action sheet, after I select one, the action wanted is in some cases the presentation of a new VC. The first example of this was a VC that displays my internal log. It's very valuable when not debugging in a "tethered" mode.
Each debugging VC is represented as a scene in the Main storyboard. I instantiate the VC with instantiateViewControllerWithIdentifier:. Then I am trying to get it presented.
The tricky part is that the new VC has to be presented and then dismissed without writing any code in the VC that is currently active. Neither do I want to create a Segue from every VC where this might be called. The whole point is that the DebugActionSheet is self contained except for the single call to fire it up.
You should be able to access the top most view controller like this from your ActionSheet delegate method.
+ (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
then in the calling code:
[MyDebugController.topMostController presentViewController:myLoggingView
animated:YES
completion:nil];
and your myLoggingView can dismiss itself by calling
[self.presentingViewController dismissViewControllerAnimated:YES
completion:nil]
Try presenting it on the main thread?
dispatch_async(dispatch_get_main_queue(), ^ {
[self presentViewController:vc animated:YES completion:nil];
});
It turned out my problem was that the current top controller is using a Navigation controller, so the required code is different.
UIStoryboard *story = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
_paletteVC = [story instantiateViewControllerWithIdentifier:#"PaletteDisplayVC"];
[[_delegate navigationController] pushViewController: _paletteVC
animated: YES];
I pass the current top controller to my DebugActionSheet as delegate, so I do not need the topMostController method above. However, I presume it would work with that also.

Setting First view controller in App Delegate

I am developing an iOS app and using storyboards. In my storyboard, I had set a view controller as the initial view controller. Everything working fine.
Now I have to write some login in app delegate to decide which view controller to show at the beginning because this depends on how far the user is in the login process.
So, I removed the initial view controller mark from my storyboard and removed the storyboard setting from my plist file.
Now, in the app delegate I have this code -
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]];
UIViewController *initialViewController = [storyboard instantiateViewControllerWithIdentifier:[XYZUtils getStartScreenViewController]];
XYZStartScreenViewController *startScreenViewController = (XYZStartScreenViewController *)initialViewController;
[self.window addSubview:startScreenViewController.view];
[self.window setRootViewController:startScreenViewController];
[self.window setBackgroundColor:[UIColor whiteColor]];
[self.window makeKeyAndVisible];
This does not display the view controller - I am just getting a black screen and no error messages. On using breakpoints to walk through the above code, there is no error. The view controller is being instantiated but it is not getting displayed.
Am I missing something here?
Read this post..
It is better you make an empty view controller and mark that as initial view controller
And do all login process in that view controller's viewDidLoad method.
Linking a new viewcontroller to Storyboard?
For some reason, when you deselect the initial view controller setting in the storyboard, then your app will not get a UIWindow setup in didFinishLaunching.
So, what you should so is instantiate your own window there; just add this to the beginning of your application didFinishLaunchingWithOptions method:
UIWindow* window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
I think this solution is perfectly fine, but a "cleaner" one since you are using the storyboard, is having some sort of "LoginManagerViewController" as your initial view controller with the responsibility of handling where to do next based on how far the user is in the login process.

viewWillDisappear not called when calling popToRootViewControllerAnimated

I work on a legacy application, and have found out, that my view[Will/Did]Disappear methods are not always fired properly.
The case is, I have a (custom) UIViewController set as rootViewController in AppDelegate. This rootViewController has a UINavigationController, which has two view controllers pushed on it. When the user presses the home button, the user is logged out. When he later returns to the app, the application calls [UINavigationController popToRootViewControllerAnimated:YES] and then displays a modal UIViewController for logging in.
The problem is: When I push/pop on the UINavigationController normally, my viewWillDisappear method is called properly. But when I use the popToRootViewControllerAnimated: method, viewWillDisappear is not called on any of the viewControllers that are popped off.
Searching on the internet has only given two possible reasons:
If using a UINavigationController as a subview, you must call view[Will/Did]Disappear yourself
Not calling the proper super methods
None of these suggestions are the case in my app. And I have no idea where to look. Anybody has a suggestion to what has been done wrong in the app?
The view probably wasn't onscreen. It has to be onscreen (visible) for the viewWillDisappear: method to be called. If it's coming back from the background, it wasn't visible.
You could try using willMoveToParentViewController: which is called when the view controller is removed from its parent.
such useful to me
[nav performSelector:#selector(popToRootViewControllerAnimated:) withObject:nil afterDelay:0.0];
I rewrote UITabBarController
- (void)setSelectedIndex:(NSUInteger)selectedIndex {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UINavigationController *navigationController = [originalViewController as:[UINavigationController class]];
if (navigationController.presentedViewController) {
[navigationController dismissViewControllerAnimated:NO completion:^{
[navigationController popToRootViewControllerAnimated:NO];
}];
}else if (navigationController.topViewController){
[navigationController popToRootViewControllerAnimated:NO];
}
});
}

Clarification on prepareforsegue and presentModalViewController while using Storyboard

I have an app with a LoginViewController as the initial view.
Note: So in appDelegate.m, self.window.rootViewController is NOT the TabBarController.
After Auth, I present the main part of the app, which has a tabbarController (identifier:tabBar) with two tabs and one tab has a navigation controller. I am using Core Data, so I need to pass MOC.
If I use,
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
UITabBarController *obj=[storyboard instantiateViewControllerWithIdentifier:#"tabBar"];
[obj setSelectedIndex:0];// Which tab to show first
[self presentModalViewController:obj animated:YES];
It works good visually. Now I need to pass the MOC. Read about PrepareToSegue method,created a segue (modal, Not shown in pic) from loginVC to my TargetViewController (TabBar>NavigationController1>View1), named the segue "LoginSegue" and used the following code:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
View1 *devicelist = (View1 *)[[navController viewControllers] lastObject];
devicelist.managedObjectContext = managedObjectContext;
}
The TabBar does not show. How do i set the tabbar controller in this case?
I have been trying to get a grasp on getting a reference for the Modal Tabbar, but still not clear. Can some one explain in layman terms how to handle a situation like this?
I think it would be better to use a design that doesn't use a modal transition to the tab bar controller. Modal presentations are generally supposed to be for interruptions to the normal flow of the app, not for getting your main controller on the screen. There are two alternatives, that I think are better. You can leave the login controller as the initial root view controller of the window, but then switch it out for the tab bar controller (which will be the new root view controller of the window, and the login controller will be deallocated). This usually works ok, but I think in this case where you want to pass the MOC from the app delegate (I presume) to a controller in the tab bar controller, I think a second way would be better.
The second way to do this, and the way I usually do login controllers, is to have the tab bar controller be the root view controller of the window, and then present the login controller modally from the viewDidAppear method of the initial view (which would be the one you're calling View1). If you do this presentation with animation set to NO, the login controller will be the first thing the user sees:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
static int first = 1;
if (first) {
LoginViewController *login = [self.storyboard instantiateViewControllerWithIdentifier:#"Login"];
[self presentViewController:login animated:NO completion:nil];
first = 0;
}
}
The if statement is in there so the presentation doesn't happen again when you come back from the login controller (you could do something more sophisticated like having a delegate call back to View1 from the login controller indicating that the login was successful if you want, but this works).
If the login succeeds, you just dismiss the login controller, and you'll be there in your first view (if it fails, you just never dismiss it, and maybe put up a message saying the login failed).
If you go this route, then you can pass the MOC in the app delegate like this:
UINavigationController *nav = [(UITabBarController *)self.window.rootViewController viewControllers][0];
View1 *devicelist = (View1 *)nav.viewControllers.lastObject;
devicelist.managedObjectContext = managedObjectContext;

Resources