iOS: How to get pointer to navigation controller - ios

I'm obviously missing something...
In my iOS app, I push a UIViewController onto a navigation controller:
MyViewController *mvc = [[MyViewController alloc] initWithNibName:#"MyViewController"];
[self.navigationController mvc animated:YES];
MyViewController displays fine, and I can see and use the navigationBar, but when I try to get a pointer back to the navigation controller from within my view controller, I get a nil result.
UINavigationController *nav = [self navigationController];
if (!nav) {
NSLog(#"no nav");
}
I've been beating my head against this all day, but can't see that I'm doing anything wrong. I get no warnings or errors in Xcode. Am I completely missing something?
TIA: john

The navigationController won't be set properly on viewDidLoad. You have to check it in viewDidAppear or at some later stage. Which method are you calling to [self navigationController] in?
The reason for this is that when viewDidLoad is called, the UINavigationController is still processing the pushViewController:animated: method. It would appear to set the navigationController property after it initialises the controller's view. I can't recall whether the property is set by the time viewWillAppear runs, but it should definitely be set by viewDidAppear.

id rootViewController = [[[[[UIApplication sharedApplication] keyWindow] subviews] objectAtIndex:0] nextResponder];

Related

Add Animation to View Change

So after a lot of research if finally found the code that allows me to change to another view without giving me any errors:
UIViewController * vc = [self.storyboard instantiateViewControllerWithIdentifier:#"gameOverPage"];
[[[[UIApplication sharedApplication] delegate] window] setRootViewController:vc];
The only problem with this code is that there is no animation. I want to somehow add the cross dissolve animation to this if possible.
Another major problem is that it shows the view two times (some times three). So it goes to the second view and then less than a second later, it shows the page again. I know this because iAd is reloaded and when I press a button that goes to another page, it is interrupted by the second page coming up again.
To change the view to another (navigating) you don't need to setRootViewController: set it as root view controller always.
you can use a UINavigationController and set a UIViewController as root, then to change view use pushViewController: method of navigation controller, like
//Pre condition - already a viewController is root view controller of navigation controller.
UIViewController * vc = [self.storyboard instantiateViewControllerWithIdentifier:#"gameOverPage"];
[self.navigationController pushViewController:vc animted:YES];
Another way is,
[self presentViewController:vc animated:YES completion:nil];
Read more presentViewController, UINavigationController

Adding child view to UITabBarController not call viewWillAppear

I have tab bar based application (iOS 7.1 SDK). When user start app at first time, I want show some login screen. I decided to use view controller containment (this is called in first view controller of tab bar controller):
LoginViewController *vc = [LoginViewController new];
[self.tabBarController addChildViewController:vc];
[vc didMoveToParentViewController:self.tabBarController];
[self.tabBarController.view addSubview:vc.view];
But there are some problems. View is normally visible, but in LoginViewController viewWillAppear and viewDidAppear are never called. I try to use this piece of code in all view lifecycle methods (viewDidLoad, viewWillAppear, viewDidAppear), but with no luck. I know there are some other ways to achieve what i'm trying to do. For example add child controller to first view controller of tab bar controller and hide tab bar, which works great and viewWillAppear and viewDidAppear are normally called. But because of this I get even more curious - why adding child view controller to tab bar controller don't work as expected?
You need to present or push the viewcontroller in order for the methods to get called. Just adding the view as a subview will not work.
In your case, you can explicitly call the viewWillAppear, viewDidAppear methods.
LoginViewController *vc = [LoginViewController new];
[self.tabBarController addChildViewController:vc];
[vc didMoveToParentViewController:self.tabBarController];
[self.tabBarController.view addSubview:vc.view];
[vc viewWillAppear];
[vc viewDidAppear];
It's working!
dispatch_async(dispatch_get_main_queue(), ^{
[self.tabBarController setSelectedIndex:0];
[self.tabBarController setSelectedIndex:1];
[self.tabBarController setSelectedIndex:0];
});

Present modal view controller from ECSlidingViewController

In my app I have an ECSlidingViewController declared as initial root controller via Storyboard. In my AppDelegate's didFinishLaunchingWithOptions method, I instantiate it as above:
self.slidingController = [[UIStoryboard storyboardWithName:#"AppStoryboard" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:#"ECSlidingViewController"];
What I want is to be able to show a global modal view controller (eg. when a push notification arrives while the app is active) without knowing which controller is currently top in the sliding controller.
What I do is (in my AppDelegate):
[self.slidingController.topViewController presentModalViewController:controller animated:YES];
but it doesn't seem to work.
Is there any way I could present a modal controller from my sliding controller regardless which controller is topViewController?
PS. If no, is there any chance that what I want will work with SWRevealViewController instead of ECSlidingViewController? If it's worth, I will take the painful road to switch.
Thank you in advance!
If the ECSlidingViewController is set as the initial view controller in the storyboard, then why are you instantiating another one in your app delegate code? By doing that, you're calling your methods on a different instance of ECSlidingViewController than the one that's put on screen by the storyboard. This is likely the source of your problem. Instead, get a reference to your ECSlidingViewController like this:
self.slidingController = self.window.rootViewController;
Then try,
self.slidingController.topViewController presentModalViewController:controller animated:YES];
or
self.slidingController presentModalViewController:controller animated:YES];
I haven't worked with ECSlidingViewController, so I don't know which of these might work.
Try this
UIViewController *rootViewController = self.window.rootViewController;
// You now have in rootViewController the view with your "Hello world" label and go button.
// Get the navigation controller of this view controller with:
UINavigationController *navigationController = rootViewController.navigationController;
[navigationController.topViewController presentModalViewController:controller animated:YES];

Can performSegueWithIdentifier be used with the AppDelegate?

I am working on an app that at launch checks for valid login credentials, and if they are found and not expired the main split view controller is displayed, and if not a login screen should be displayed.
Each part is working fine separately, but I am struggling with the best way at launch time to select the proper view to display.
I have tried setting up a modal segue from the root view controller, and in my application:didFinishLaunchingWithOptions: function in the App Delegate, calling this:
// Segue to the login view controller...
if (loginNeeded) {
[self.window.rootViewController performSegueWithIdentifier:#"LoginScreen" sender:self];
}
This logically should work, but triggering segues from within the app delegate seems to be impossible.
What is the ideal place and technique for handling this?
You could try a custom segue, as per this post hiding-a-segue-on-login-process.
Alternatively if you're desperate to have the login display before the split view controller loads try something along the following lines...
Create your login screen on the main storyboard as, say, a subclass of UIViewController. Make sure it is the initial scene (check Is Initial View Controller).
On the storyboard, create a new segue from your login class to the original SplitViewController. Give it an identifier, 'Load SplitViewController' and a segue custom class name which we'll call FullyReplaceSegue.
In your login class .m file, add code to be called once the user has logged in:
[self performSegueWithIdentifier:#"Load SplitViewController" sender:self];
Create the new segue class, based on UIStoryboardSegue and name it FullyReplaceSegue as per above.
.h file
#import <UIKit/UIKit.h>
#interface : UIStoryboardSegue
#end
.m file
#import "FullyReplaceSegue.h"
#implementation FullyReplaceSegue
- (void)perform
{
UIViewController *dest = (UIViewController *) super.destinationViewController;
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = dest;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
UISplitViewController *splitViewController = (UISplitViewController *)dest; // assumes we're transitioning to a UISplitViewController!
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
splitViewController.delegate = (id)navigationController.topViewController;
}
}
#end
Here's how I did it.
In didFinishLaunchingWithOptions:
//save the root view controller
[[self window] makeKeyAndVisible];
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
rootController = [[navigationController viewControllers] objectAtIndex:0];
Somewhere else in the app delegate:
[rootController performSegueWithIdentifier:#"fileSegueID" sender:self];
Then, in the storyboard, create a segue from the view that gets assigned as "rootController", to the desired optional view, and give that new segue the id fileSegueID. It takes some debugging to make sure the rootController variable gets assigned to the correct view.
Maybe a little late, but I was looking for the same suggestions. Here's what I wound up doing.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Signup" bundle:nil];
if(isLoggedIn) {
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
IndexController *ivc = [storyboard instantiateViewControllerWithIdentifier:#"IndexController"];
[navigationController pushViewController:ivc animated:NO];
}
Why don't you load the screen that would be visible assuming proper and non-expired log-in credentials (by setting it as the root view controller of the window), and then in viewDidLoad of that first view controller, check if an update to the login credentials are needed. If so, segue into the login view controller.
Yes, it can be used, if you get a reference to the segue's parent view controller. You can get it like this:
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
[[[navigationController viewControllers] objectAtIndex:0] performSegueWithIdentifier:#"LoginScreen" sender:self];
This will only work if the index in viewControllers array matches the one of your view controller and if it exists of course. In this case is the first one (in the array and storyboard).
The segue ("LoginScreen") must not be attached to an action. The way you do this is by control-dragging from the file owner icon at the bottom of the storyboard scene to the destination scene. A popup will appear that will ask for an option in “Manual Segue”; pick “Push” as the type. Tap on the little square and make sure you’re in the Attributes Inspector. Give it an identifier which you will use to refer to it in code.

Unbalanced calls to begin/end appearance transitions for <UITabBarController: 0x197870>

I read SO about another user encountering similar error, but this error is in different case.
I received this message when I added a View Controller initially:
Unbalanced calls to begin/end appearance transitions for
<UITabBarController: 0x197870>
The structure of the app is as follow:
I got a 5-tab TabBarController linked to 5 View Controllers. In the initial showing tab, I call out a new View Controller to overlay as an introduction of the app.
I use this code to call the introduction view controller:
IntroVC *vc = [[IntroVC alloc] init];
[self presentModalViewController:vc animated:YES];
[vc release];
After this IntroVC view controller shows up, the above error shows.
p.s. I am using xCode 4.2 & iOS 5.0 SDK, developing iOS 4.3 app.
Without seeing more of the surrounding code I can't give a definite answer, but I have two theories.
You're not using UIViewController's designated initializer initWithNibName:bundle:. Try using it instead of just init.
Also, self may be one of the tab bar controller's view controllers. Always present view controllers from the topmost view controller, which means in this case ask the tab bar controller to present the overlay view controller on behalf of the view controller. You can still keep any callback delegates to the real view controller, but you must have the tab bar controller present and dismiss.
I fixed this error by changing animated from YES to NO.
From:
[tabBarController presentModalViewController:viewController animated:YES];
To:
[tabBarController presentModalViewController:viewController animated:NO];
As posted by danh
You can generate this warning by presenting the modal vc before the app is done initializing. i.e. Start a tabbed application template app and present a modal vc on top of self.tabBarController as the last line in application:didFinishLaunching. Warning appears. Solution: let the stack unwind first, present the modal vc in another method, invoked with a performSelector withDelay:0.0
Try to move the method into the viewWillAppear and guard it so it does get executed just once (would recommend setting up a property)
Another solution for many cases is to make sure that the transition between UIViewControllers happens after the not-suitable (like during initialization) procedure finishes, by doing:
__weak MyViewController *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf presentViewController:vc animated:YES];
});
This is general for also pushViewController:animated:, etc.
I had the same problem. I called a method inside viewDidLoad inside my first UIViewController
- (void)viewDidLoad{
[super viewDidLoad];
[self performSelector:#selector(loadingView)
withObject:nil afterDelay:0.5];
}
- (void)loadingView{
[self performSegueWithIdentifier:#"loadedData" sender:self];
}
Inside the second UIViewController I did the same also with 0.5 seconds delay. After changing the delay to a higher value, it worked fine. It's like the segue can't be performed too fast after another segue.
I had the same problem when I need to Present My Login View Controller from another View Controller If the the User is't authorized, I did it in ViewDidLoad Method of my Another View Controller ( if not authorized -> presentModalViewController ). When I start to make it in ViewDidAppear method, I solved this problem. I Think that ViewDidLoad only initialize properties and after that the actual showing view algorithm begins! Thats why you must use viewDidAppear method to make modal transitions!
If you're using transitioningDelegate (not the case in this question's example), also set modalPresentationStyle to .Custom.
Swift
let vc = storyboard.instantiateViewControllerWithIdentifier("...")
vc.transitioningDelegate = self
vc.modalPresentationStyle = .Custom
I had this problem because of a typo:
override func viewDidAppear(animated: Bool) {
super.viewWillAppear(animated)
instead of
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
It was calling "WillAppear" in the super instead of "DidAppear"
I had lot of problem with the same issue. I solved this one by
Initiating the ViewController using the storyboad instantiateViewControllerWithIdentifier method. i.e Intro *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"introVC"];
[self.tabBarController presentModalViewController : vc animated:YES];
I have the viewcontroller in my storyboard, for some reason using only [[introvc alloc] init]; did not work for me.
I solved it by writing
[self.navigationController presentViewController:viewController
animated:TRUE
completion:NULL];
I had this problem with a third party code. Someone forgot to set the super inside of viewWillAppear and viewWillDisappear in a custom TabBarController class.
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// code...
}
or
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// code...
}
I had the same error. I have a tab bar with 3 items and I was unconsciously trying to call the root view controller of item 1 in the item 2 of my tab bar using performSegueWithIdentifier.
What happens is that it calls the view controller and goes back to the root view controller of item 2 after a few seconds and logs that error.
Apparently, you cannot call the root view controller of an item to another item.
So instead of performSegueWithIdentifier
I used [self.parentViewController.tabBarController setSelectedIndex:0];
Hope this helps someone.
I had the same problem and thought I would post in case someone else runs into something similar.
In my case, I had attached a long press gesture recognizer to my UITableViewController.
UILongPressGestureRecognizer *longPressGesture = [[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:#selector(onLongPress:)]
autorelease];
[longPressGesture setMinimumPressDuration:1];
[self.tableView addGestureRecognizer:longPressGesture];
In my onLongPress selector, I launched my next view controller.
- (IBAction)onLongPress:(id)sender {
SomeViewController* page = [[SomeViewController alloc] initWithNibName:#"SomeViewController" bundle:nil];
[self.navigationController pushViewController:page animated:YES];
[page release];
}
In my case, I received the error message because the long press recognizer fired more than one time and as a result, my "SomeViewController" was pushed onto the stack multiple times.
The solution was to add a boolean to indicate when the SomeViewController had been pushed onto the stack. When my UITableViewController's viewWillAppear method was called, I set the boolean back to NO.
I found that, if you are using a storyboard, you will want to put the code that is presenting the new view controller in viewDidAppear. It will also get rid of the "Presenting view controllers on detached view controllers is discouraged" warning.
In Swift 2+ for me works:
I have UITabBarViewController in storyboard and I had selectedIndex property like this:
But I delete it, and add in my viewDidLoad method of my initial class, like this:
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.selectedIndex = 2
}
I hope I can help someone.
This error will be displayed when trying to present an UINavigationController that is lazily initialized via a closure.
Actually you need to wait till the push animation ends. So you can delegate UINavigationController and prevent pushing till the animation ends.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
waitNavigation = NO;
}
-(void)showGScreen:(id)gvc{
if (!waitNavigation) {
waitNavigation = YES;
[_nav popToRootViewControllerAnimated:NO];
[_nav pushViewController:gvc animated:YES];
}
}
As #danh suggested, my issue was that I was presenting the modal vc before the UITabBarController was ready. However, I felt uncomfortable relying on a fixed delay before presenting the view controller (from my testing, I needed to use a 0.05-0.1s delay in performSelector:withDelay:). My solution is to add a block that gets called on UITabBarController's viewDidAppear: method:
PRTabBarController.h:
#interface PRTabBarController : UITabBarController
#property (nonatomic, copy) void (^viewDidAppearBlock)(BOOL animated);
#end
PRTabBarController.m:
#import "PRTabBarController.h"
#implementation PRTabBarController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.viewDidAppearBlock) {
self.viewDidAppearBlock(animated);
}
}
#end
Now in application:didFinishLaunchingWithOptions:
PRTabBarController *tabBarController = [[PRTabBarController alloc] init];
// UIWindow initialization, etc.
__weak typeof(tabBarController) weakTabBarController = tabBarController;
tabBarController.viewDidAppearBlock = ^(BOOL animated) {
MyViewController *viewController = [MyViewController new];
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[weakTabBarController.tabBarController presentViewController:navigationController animated:NO completion:nil];
weakTabBarController.viewDidAppearBlock = nil;
};
you need make sure -(void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated and -(void)endAppearanceTransition is create together in the class.
I had the same issue. When developing I wanted to bypass screens. I was navigating from one view controller to another in viewDidLoad by calling a selector method.
The issue is that we should let the ViewController finish transitioning before transitioning to another ViewController.
This solved my problem: The delay is necessary to allow ViewControllers finish transitioning before transitioning to another.
self.perform(#selector(YOUR SELECTOR METHOD), with: self, afterDelay: 0.5)
For me this error occurred because i didn't have UIWindow declared in the upper level of my class when setting a root view controller
rootViewController?.showTimeoutAlert = showTimeOut
let navigationController = SwipeNavigationController(rootViewController: rootViewController!)
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
Ex if I tried declaring window in that block of code instead of referencing self then I would receive the error
I had this problem when I had navigated from root TVC to TVC A then to TVC B. After tapping the "load" button in TVC B I wanted to jump straight back to the root TVC (no need to revisit TVC A so why do it). I had:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:YES];
//Pop self to return to root
[self.navigationController popViewControllerAnimated:YES];
...which gave the error "Unbalanced calls to begin/end etc". The following fixed the error, but no animation:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:NO];
//Then pop self to return to root
[self.navigationController popViewControllerAnimated:NO];
This was my final solution, no error and still animated:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:NO];
//Then pop self to return to root, only works if first pop above is *not* animated
[self.navigationController popViewControllerAnimated:YES];
I encountered this error when I hooked a UIButton to a storyboard segue action (in IB) but later decided to have the button programatically call performSegueWithIdentifier forgetting to remove the first one from IB.
In essence it performed the segue call twice, gave this error and actually pushed my view twice. The fix was to remove one of the segue calls.
Hope this helps someone as tired as me!
Swift 5
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//Delete or comment the below lines on your SceneDelegate.
// guard let windowScene = (scene as? UIWindowScene) else { return }
// window?.windowScene = windowScene
// window?.makeKeyAndVisible()
let viewController = ListVC()
let navViewController = UINavigationController(rootViewController: viewController)
window?.rootViewController = navViewController
}

Resources