First off, I am aware of this question being asked in a forward manner, but in this case I am asking for a backwards manner in which the Navigation Controller is already designed. With that being said...
I have a UINavigationController with three views: Table, Get, and Avail in that order that was created in IB.
When going forward, I want to go from Table to Get to Avail, but when I hit the "Back" button on Avail I want to skip over Get and go directly back to Table. Is this possible? If so, how?
Here's how I did it:
NSArray *VCs = [self.navigationController viewControllers];
[self.navigationController popToViewController:[VCs objectAtIndex:([VCs count] - 2)] animated:YES];
To be able to override the nav controllers's back button you're going to have to subclass UINavigationController. Check out how in this tutorial: http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller
Implement the navigation controller's delegate method:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
It will fire for all VCs in the nav controller stack. (put this code in the VC immediately after your navigation controller, and put < UINavigationControllerDelegate > in the .h)
What you want to do is replace the entire Navigation Controller's stack without the view controller you want to remove in it. Since it's a non-mutable array, you have to convert it to a mutable array before removing the VC, remove the VC, then replace the navigation controller's stack with the call [self.navigationController setViewControllers:newVCs animated:NO];. That's the crucial part. You are replacing the stack after you have loaded the page you are on but since you're keeping the VC you're on, it's still the top item on the stack so there is no visible impact to the user. As long as you don't have a lot of VCs on the stack, it's not an expensive call.
Here's how I did it in the delegate method:
//Remove list setup page if new list was created
if ([self.navigationController topViewController].class == [ItemViewController class])
{
NSArray *VCs = [self.navigationController viewControllers];
if(((UITableViewController*)[VCs objectAtIndex:[VCs count]-2]).class == [NewCardTypeController class])
{
NewCardTypeController *removedObject = [VCs objectAtIndex:[VCs count]-2];
if(removedObject != nil)
{
NSMutableArray *newArray = [NSMutableArray arrayWithArray:VCs];
[newArray removeObject:removedObject];
NSArray *newVCs = [NSArray arrayWithArray:newArray];
[self.navigationController setViewControllers:newVCs animated:NO];
}
}
}
Take a look at UINavigationController's -popToViewController:animated: and -popToRootViewControllerAnimated:, which do exactly what you're asking for. That is, they pop the navigation stack back to a particular view controller, or to the root view controller. You'll still need to intercept the nav controller's back button action to use them, though.
Related
I'm working on my first app. Here's what I want to accomplish:
There will be a menu with several different options. For simplicity, assume this is comprised of UIButtons with IBAction outlets and the functionality exists to pull up the menu at any time.
Each menu button, when pressed, should display a different navigation controller's content. If the user brings up the menu and makes a different selection, the navigation controller in which he is currently operating should not be affected; the newly selected navigation chain is displayed on top of the old, and through the menu, the user can go back to the view where he left off on the previous navigation chain at any time.
visual illustration (click for higher resolution):
Please note that there are 3 different navigation controllers/chains. The root view controller (which is also the menu in this simplified version) is not part of any of them. It will not suffice to instantiate one of the navigation chains anew when it has been previously instantiated, and here's why: if the user was on screen 3 of option 2 and then selects option 1 from the menu and then selects option 2 (again) from the menu, he should be looking at screen 3 of option 2--right where he left off; the view controller he was viewing when he previously left the navigation chain should be brought back to the top.
I can make a button instantiate and present a view controller from the storyboard if there is NOT a navigation controller:
- (IBAction)buttonPressed:(id)sender {
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
[self presentViewController:controller animated:YES completion:nil];
}
However, I can't figure out how to make those two methods work with a navigation controller involved. Moreover, I'm not sure those two methods are the right choice, because I won't always want to instantiate a new view controller: when a menu button is pressed, a check should be performed to see if the view (navigation?) controller with the corresponding identifier has already been instantiated. If so, it should simply be made the top view controller.
In summary, here are my questions:
1) How should I instantiate and display a view controller that is embedded in a navigation controller, preferably using a storyboard ID? Do you use the storyboard ID of the navigation controller or of the view controller?
2) How should I check whether an instance already exists? Again, should I check for an extant navigation controller or for a view controller, and what's the best method to do so?
3) If the selected navigation chain has already been instantiated and is in the stack of view controllers somewhere, what is the best method for bringing it to the top?
Thank you!!
side note -- it would be nice to know how to paste code snippets with indentation and color formatting preserved :)
As Rob has suggested, a tab bar controller would make a good organising principle for your design.
Add a UITabBarController to your storyboard, give it a storyboard iD. Assign each of your three sets of viewControllers ( with their respective navController) to a tab item in the tabBarController.
UITabBarController
|--> UINavigationController --> VC1 ---> VC2 -->
|--> UINavigationController --> VC1 ---> VC2 -->
|--> UINavigationController --> VC1 ---> VC2 -->
In you app delegate make a strong property to hold your tab bar controller's pointer. As the tab bar controller keeps pointers to all of it's tab items, this will take care of state for each of your sets of viewControllers. You won't have to keep separate pointers for any of them, and you can get references to them via the tabBarController's viewControllers property.
#property (strong, nonatomic) UITabBarController* tabVC;
Initialise it on startup
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIStoryboard storyBoard =
[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
self.tabVC = [storyBoard instantiateViewControllerWithIdentifier:#"tabVC"];
//hide the tab bar
for (UINavigationController* navController in self.tabVC.viewControllers)
[navController.viewControllers[0] setHidesBottomBarWhenPushed:YES];
return YES;
}
An alternative way to hide the tab bar is to check the "Hides bottom bar on push" box in the Attributes Inspector for each of the (initial) viewControllers. You don't have to do this for subsequent viewControllers, just the first one that will be seen in that tab item.
Then when you need to navigate to one of your navController groups…
- (IBAction)openTab:(UIButton*)sender {
AppDelegate* appDelegate =
(AppDelegate*)[[UIApplication sharedApplication] delegate];
if ([sender.titleLabel.text isEqualToString: #"Option 1"]) {
appDelegate.tabVC.selectedIndex = 0;
}else if ([sender.titleLabel.text isEqualToString: #"Option 2"]){
appDelegate.tabVC.selectedIndex = 1;
}else if ([sender.titleLabel.text isEqualToString: #"Option 3"]){
appDelegate.tabVC.selectedIndex = 2;
}
[self presentViewController:appDelegate.tabVC
animated:YES completion:nil];
}
(this example uses presentViewController, your app design may navigate this in other ways…)
update
If you want to do this without a tab bar controller, you can instantiate an array holding pointers to each of your nav controllers instead:
UINavigationController* ncA =
[storyboard instantiateViewControllerWithIdentifier:#"NCA"];
UINavigationController* ncB =
[storyboard instantiateViewControllerWithIdentifier:#"NCB"];
UINavigationController* ncC =
[storyboard instantiateViewControllerWithIdentifier:#"NCC"];
self.ncArray = #[ncA,ncB,ncC];
Which has the benefit of not having a tab bar to hide…
Then your selection looks like…
- (IBAction)openNav:(UIButton*)sender {
AppDelegate* appDelegate =
(AppDelegate*)[[UIApplication sharedApplication] delegate];
int idx = 0;
if ([sender.titleLabel.text isEqualToString: #"option 1"]) {
idx = 0;
}else if ([sender.titleLabel.text isEqualToString: #"option 2"]){
idx = 1;
}else if ([sender.titleLabel.text isEqualToString: #"option 3"]){
idx = 2;
}
[self presentViewController:appDelegate.ncArray[idx]
animated:YES completion:nil];
}
1 / You can instantiate a viewController in your viewDidLoad method of your main viewController, so it will be instantiate 1 time only.
Now if you want display your controller, you would better push it :
- (IBAction)buttonPressed:(id)sender {
// Declare your controller in your .h file and do :
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
// Note you can move this line in the viewDidLoad method to be called only 1 time
// Then do not use :
// [self presentViewController:controller animated:YES completion:nil];
// Better to use :
[self.navigationController pushViewController:controller animated:YES];
}
2 / I'm not sure, but if you want to check if an instance already exist just check :
if (controller) {
// Some stuff here
} // I think this checks if controller is initiated.
3 / I know it's not a good advice but I would tell you to not worry about checking if your controller already exist, because I think it's easier to access your viewController by using the 2 lines again :
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
[self.navigationController pushViewController:controller animated:YES];
4 / I'm not sure if colors can be used here because of a specific style sheets.
I'm not sure to really have the good answer to your question but I hope this will help you.
I have 2 views, TableController and WirelessController. While in TableController I need to pop the WirelessController view. This is what I've tried and nothing happens, no console output either.
WirelessController *wCon = [[WirelessController alloc] init];
[[wCon navigationController] popViewControllerAnimated:YES];
and this has the same problem.
[self navigationController] popViewControllerAnimated:YES];
Is it the fact that I'm using the UINavigationController when it's a view based app?
I think you mean to push, not pop...
WirelessController *wCon = [[WirelessController alloc] init];
[[self navigationController] pushViewControllerAnimated:YES];
Push adds a new item to the top of the stack; pop removes the top item from the stack.
Update
From your comments it seems that...
your first view is an instance of WirelessController.
from there you present a TableController modally
now you want to return to you wirelessController.
In this case you need to send a message back to the presenting view controller (wirelessController) asking it to dismiss the view controller it has presented (tableController)
In tableController:
[self presentingViewController] dismissViewControllerAnimated:YES
completion:nil]];
Whatever is going on, you certainly don't want to do this:
WirelessController *wCon = [[WirelessController alloc] init];
This line will create a new object. You want to return to an existing object.
Pushing and popping viewControllers is an activity generally associated with navigation controllers, which keep an array of managed viewControllers. In that case you would push to add a new controller to the top of the stack, and pop to remove it from the stack. In the absence of a navigation controller, there is no such stack, so push and pop make no sense.
I am just wondering how I could programmatically navigate around a navigationController stack?
I am familiar with the method:
[self.navigationController popToRootViewControllerAnimated:YES];
as well as:
[self.navigationController popViewControllerAnimated:YES];
But could programatically go to ANY view controller in my navigation controller?
Please see my pic:
http://s18.postimage.org/iq1l4f721/test_xcodeproj_Main_Storyboard_storyboard.jpg?
I know I can segue to any view using the storyboard but am I right in thinking this would keep pushing new views onto the stack and eventually (in theory) I would run into problems?
Thanks
Carl.
Yes , if you know the index of the controller in the stack or if you have a reference to it.
Like this:
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:index] animated:TRUE];
Cheers!
have you tried
for (UIViewController * viewController in self.navigationController.viewControllers) {
if ([viewController isKindOfClass:[Number2Class class]]) {
[self.navigationController popToViewController:viewController animated:YES];
}
}
Use this code to navigate to the desired viewcontroller
NSArray *vcs = self.navigationController.viewControllers;
for (UIViewController *cont in vcs) {
if([cont class] == [YOUR_VIEWCONTROLLER_NAME class])
{
NSLog(#"Class Found");
}
Have you read UINavigationController Class reference?
UINavigationController has property NSArray *viewControllers, which holds current stack of view controllers (history).
setViewControllers:animated: - Replaces the view controllers currently managed by the navigation controller with the specified items. (overrides history)
popToViewController:animated: - Pops view controllers until the specified view controller is at the top of the navigation stack. (here you will need to pass an instance that exists in the history - see above)
Get back to #1...
I have a UINavigationController that gets a few views pushed onto the stack. Once I am a few levels in, I need to call up a modal view that is a copy of the UINavigationController and is at the same level in as the calling navigation controller.
Is this possible?
You should not interact with the tableView directly if it is not currently visible. (i.e. when another ViewController is on top of the stack)
A preferable solution to the problem you described in the comments is to modify the datasource and reload the tableView as soon as it is shown.
Bettr to add an button at the leftnavigationitem in each view and write action for popto RootView to remove the stack
[self.navigationController popToRootViewControllerAnimated:YES];
add back button in viewdidload
//To set the back buttin on Navigation bar
UIBarButtonItem *backButton = [[[UIBarButtonItem alloc] initWithTitle:#"<--" style:UIBarButtonItemStyleBordered target:self action:#selector(backclick:)] autorelease];
self.navigationItem.leftBarButtonItem = backButton
Now implwment action
- (IBAction)backclick:(id)sender
{
// To goback to the main view
[self.navigationController popToRootViewControllerAnimated:YES];
}
Yes, this is possible.
To push a new navigation stack modally, create a new navigation controller and populate its stack (setViewControllers:) with the views you want. You could use the same VC instances in this new stack, but I suggest instead new instances (perhaps of the same classes). The old stack will be hidden so long as this new, modal stack is in place. Dismiss the NavCon to get your old stack back.
If you just want to replace the VC in the current stack, you can pop the current VC (probably not animated!) and then push the new one. The user will be able to navigate through the existing stack using the left button in the nav bar, and navigate forward as you've implemented it.
Calling UINavigationController:setViewControllers replaces a navcon's entire stack with one call. This transition can be animated or not at your discretion.
Please allow me to summarize the answer with a little code snippet:
UIViewController *previousVC = nil;
if ([self.navigationController.viewControllers count] > 1)
previousVC =
[self.navigationController.viewControllers objectAtIndex:
([self.navigationController.viewControllers count] -2)];
else
previousVC = [self.navigationController.viewControllers objectAtIndex:0];
I'm having a problem with an iPhone App using UINavigationController. When I'm using pushNavigationController, it works fine. The iPhone does its animation while switching to the next ViewController. But when using an array of ViewControllers and the setViewControllers method, it has a glitch in the animation which can grow into a clearly visible animation bug.
The following snippet is called in the root ViewController. Depending on a condition it should either switch to ViewController1, or it should directly go to ViewController2. In the latter case the user can navigate back to vc1, then to the root.
NSMutableArray* viewControllers = [NSMutableArray arrayWithCapacity:2];
// put us on the stack
[viewControllers addObject:self];
// add first VC
AuthentificationViewController* authentificationViewController =
[[[AuthentificationViewController alloc] initWithNibName:#"AuthentificationViewController" bundle:nil] autorelease];
[viewControllers addObject:authentificationViewController];
if (someCondition == YES)
{
UserAssignmentsListViewController* userAssignmentsListViewController =
[[[UserAssignmentsListViewController alloc] initWithNibName:#"UserAssignmentsOverviewViewController" bundle:nil] autorelease];
[viewControllers addObject:userAssignmentsListViewController];
}
[self.navigationController
setViewControllers:[NSArray arrayWithArray:viewControllers] animated:YES];
As you can see I'll add the first and maybe the second VC to the array, finally setting the navigationController stack with animation. This works properly if I only add the first controller. But in the case where the animation should go to the 2nd controller, the navigation bar's title won't be "flying in". Instead there is an empty title until the animation is finished. And, even worse, if I replace the navbar title with a custom button, this button will be displayed in the upper left corner until the animation is finished. That's quite a displaying bug.
I tried to use a workaround with multiple pushViewController methods, but the animation doesn't look / feel right. I want the navigation to do its animation in the same way as pushViewController does. The only difference here is, that I don't add a VC but set the whole stack at once. Is there another workaround here, or could this be considered as a framework's bug? I thought about using only pushNavController for VC2, then somehow insert VC1 into the stack, but that doesn't seem possible.
Thanks for all hints and advices. :-)
Technical data: I'm using iOS 4.2, compiling for 4.0.
Finally I found the solution. The mistake was that the new top-level NavigationController has not been initialized and loaded properly until the animation is done. In my case, UserAssignmentsListViewController has a viewDidLoad method that will not be called until animation is done, but it sets the navigation title (here: a UIButton). Therefore the animation fails.
The solution is to refer to an already initialized view controller when it comes to pushing it to the stack. So initialize our top-level VC somewhere:
// initialize our top-level controller
ViewController* viewController2 = [[[ViewController alloc]
initWithNibName:#"ViewController" bundle:nil] autorelease];
Then when pushing two or more VCs to the stack, the top level one is already initialized and the animation works (following the example from my original question):
NSMutableArray* viewControllers = [NSMutableArray arrayWithCapacity:2];
// put us on the stack, too
[viewControllers addObject:self];
ViewController* viewController1 = [[[ViewController alloc]
initWithNibName:#"ViewController" bundle:nil] autorelease];
[viewControllers addObject:viewController1];
if (someCondition == YES)
{
[viewControllers addObject:viewController2];
}
[self.navigationController
setViewControllers:[NSArray arrayWithArray:viewControllers] animated:YES];