#try {
NSArray *viewContrlls=[[self navigationController] viewControllers];
NSUInteger totalelement = [viewContrlls count];
UIViewController *LastElementController = [viewContrlls objectAtIndex:totalelement-2];
[self GotoDifferentViewWithAnimation:LastElementController];
} #catch (NSException * e) {
NSLog(#"Exception: %#", e);
}
I am trying to get the last visited viewcontroller and pushback there with animation using the above code. It's showing the last visited viewcontroller properly like,
LastElementController --- <RRLoginViewController: 0x10b324e60>
But getting complietime err,
Pushing the same view controller instance more than once is not supported (<RRLoginViewController: 0x10b324e60>)
any help.....
You're trying to push a view controller that's already in the navigation stack. Hence, you cannot "go back" to the view controller via "pushing it again".
[self.navigationController popToViewController:LastElementController animated:YES];
will do the job, but I think you want to save the condition of that View Controller.
Related
I have a UIViewController called 'detailViewController'.
This view controller is accessed through multiple different segues using the push segue.
The problem I am facing is that the back button on the detailViewController takes the user back to the previous view controller (as it should), however I would like the back button to take the user to the masterViewController (the default UIViewController in the app).
How do I do this?
I have tried changing the segue types however that didn't really do anything at all.
Peter
The method you're looking for is UINavigationController's popToRootViewControllerAnimated:
From the documentation: "Pops all the view controllers on the stack except the root view controller and updates the display."
You'll need to create a custom back button. You can't afaik override the back button's functionality.
So something like:
UIButton *myBackButton = [UIButton buttonWithType:UIButtonTypeCustom];
[myBackButton addTarget:self action:#selector(popToRoot:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *myCustomBackButtonItem = [[UIBarButtonItem alloc] initWithCustomView:myBackButton];
[self.navigationItem setLeftBarButtonItem:myCustomBackButtonItem];
and then the implementation of popToRoot: would look like:
- (void)popToRoot:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}
If the masterViewController you're talking about is the root you can call:
[self.navigationController popToRootViewControllerAnimated:YES];
You can create the back button yourself and call [self.navigationController popToRootViewControllerAnimated:YES]. However, that does require you to create the back button yourself. An alternative option could be to take the current view controller, and remove all of them except the first and current ones:
NSMutableArray *viewControllers = [self.navigationController.viewControllers mutableCopy];
[viewControllers removeObjectsInRange:NSRangeMake(1, [viewControllers count] - 2)];
self.navigationController.viewControllers = viewControllers;
Use the following code in order to go back to main view controller:
[self.navigationController popViewControllerAnimated:YES];
Edit:
Based on the comments, I realize that my code takes you only one step back and may not answer what is asked in this question. Thanks.
I came up with a workaround that looks up in a navigation controller's collection and find the offset between the existing and destination controller using name of destination controller and remove the in-between controller. Not sure if it is close to your need but give it a try:
- (NSArray *)navigateToViewController:(NSString *)destinationControllerName withControllers:(NSArray *)sourceControllers
{
NSMutableArray *controllers = [[NSMutableArray alloc] initWithArray:sourceControllers];
NSInteger sourceControllerIndex = [controllers count] - 1; // Value should be index oriented instead of position oriented.
NSInteger destControllerIndex = 0;
// Find position of destination controller (index oriented)
for (UIViewController *controller in controllers)
{
NSString *controllerName = NSStringFromClass([controller class]);
NSLog(#"class name: %#", controllerName);
if([[controllerName lowercaseString] isEqualToString:[destinationControllerName lowercaseString]])
break;
destControllerIndex +=1;
}
// If desired controller does not exists in the stack, do not proceed.
if (destControllerIndex == sourceControllerIndex)
{
return controllers;
}
// If destination is the same as source do not enter in this block
// If destination and source controllers have zero or no controllers in between do not enter in this block
// If destination and source controllers has few controllers (at least one) in between, go inside this block.
// Here "destControllerIndex + 1" shows, there is at least one controller in between source and destination.
else if(destControllerIndex + 1 < sourceControllerIndex)
{
NSLog(#"controllers in stack: %#", controllers);
// Start from the controller following the source controller in stack and remove it
// till the destination controller arrives
for (NSInteger iterator = sourceControllerIndex - 1; iterator > destControllerIndex; iterator--)
{
UIViewController *cont = [controllers objectAtIndex:iterator];
if(cont) {
[cont release]; cont = nil; }
[controllers removeObjectAtIndex:iterator];
NSLog(#"controllers in stack: %#", controllers);
}
}
return controllers;
}
and define it in some base controller:
-(void) popToViewController:(NSString *)controllerName withNavController:(UINavigationController *)navController
{
// STStatistics refers a singleton class of common functions.
NSArray *mArr = [STStatistics navigateToViewController:controllerName withControllers:navController.viewControllers];
navController.viewControllers = mArr;
// There should be more than one controller in stack to pop.
if([mArr count] > 1)
{
[navController popViewControllerAnimated:YES];
}
}
and here is how called:
[self popToViewController:#"NameOfDestinationControllerInNavStack" withControllers:self.navigationController];
I have created a single view based application. I was wanting to overlay UIViewControllers as I needed them, for instance like a modal view thing where if some values are populated then load the next view until you do something there and you can come back.
This is the code I have
- (void) viewWillAppear:(BOOL)animated {
NSMutableDictionary *tempPrefs = [prefsController readPrefs];
NSString *tempName = [tempPrefs objectForKey:#"Name"];
NSString *tempProduct = [tempPrefs objectForKey:#"Product"];
// usedbefore so skip first view (first view == login view
if ((tempName.length != 0) && (tempProduct.length != 0)) {
// you have values, enter new room without checking
[self loadGetProListViewController];
}
}
- (void) loadGetProListViewController {
[self dismissViewControllerAnimated:NO completion:nil];
getProListViewController = [[GetProListViewController alloc] initWithNibName:#"GetProListViewController" bundle:nil];
[self presentViewController:getProListViewController animated:YES completion:nil];
}
However once this method has been reached its executed but nothing is happening..
If anyone could tell me how to create modal viewControllers or some description that would be greatly appreciated.
First you should be aware of UIViewController lifecycle to have an idea where you are able to present different UIViewController
Also I guess you have a bit incorrect architecture (maybe I wrong).
Let's imagine you have firstViewController on which -viewDidAppear method you present getProListViewController
Don't you think that presenting intermediate firstViewController is odd?
I'd reather you would move your if clause to level up. F.e:
/ usedbefore so skip first view (first view == login view
if ((tempName.length != 0) && (tempProduct.length != 0)) {
// present your getProListViewController
} else {
// present your "FirstViewController" (login or whatever)
}
And you haven't faced with your issue at the beginning
Try this
[self presentModalViewController: getProListViewController animated:YES];
instead of your presentviewcontroller
[self presentViewController:getProListViewController animated:YES completion:nil];
I am trying to use popToViewController and it I keep getting the error "Tried to pop to a view controller that doesn't exist"?
I am in a Settings view and when the user clicks "Sign Out" I dismiss the Settings VC and segue back to the mainView where an unwind segue method is called. In the unwind segue method I call the following.
-(IBAction)endSettingsViaLogout:(UIStoryboardSegue *)segue {
//[self performSegueWithIdentifier:#"mainToLoginSegue" sender:self];
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
//[self.navigationController popViewControllerAnimated:YES];
DLog(#"User finished with search");
}
When the poptoVC is called I get the "Tried to pop to a view controller that doesn't exist".
I NSLog the self.navigationController.viewControllers and I can see the stack of VC's and the one I want to pop to is in there?
/// UPDATE //////
Ok here is what I have found. If my segue to Settings is a regular "push" segue then the code works and I get popped back to where I want. If I do a custom segue, having come from the left side of the screen then it stops working. Even with the custom segue the self.navigationcontroller.viewcontrollers shows its in the stack. So why can't I pop back to it? Or how can I pop back to it with the custom segue?
Do I get the view controller hierarchy right?
On your basis UINavigationViewController you have set a main view controller as root view controller.
Then a settings view controller has been pushed upon this.
Via "Sign out" the settings view controller is being segued back to the main view controller.
If so, you are actually trying pop "back" to a view controller, that does not exist, since you already have reached the root view controller of your navigation controller stack. In this case, all previously initialized controllers have been popped from the stack and you would have to reinitialize and push the desired view controller.
If I am missing some important point, it would be helpful, if you would describe your actual view controller stack at the moment the "Sign out" option is available. Furthermore, what exactly is printed on the console if you log the self.navigationController.viewControllers array?
well that basically tells objectatindex 1 does not exist:
things to try:
objectatindex:0
or
nslog(#"%d",[[self.navigationController.viewControllers]count]);//add it before the popline and see if it works
if its the topview controller then try below:
[navigationController topViewController] instead
NSArray *viewControllers = [[self navigationController] viewControllers];
for( int i=0;i<[viewControllers count];i++)
{
id obj=[viewControllers objectAtIndex:[viewControllers count]-i-1];
if([obj isKindOfClass:[OrderCheckOutViewController class]]){
[[self navigationController] popToViewController:obj animated:YES];
return;
}
}
you can use the snippet to pop out to targetVC's next viewController in the navigationController's stack.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
BOOL findIt = NO;
UIViewController *targetVC = nil;
for (UIViewController *subVC in self.navigationController.viewControllers) {
if (findIt) {
break;
}
if (subVC == xxx) {
findIt = YES;
}else{
targetVC = subVC;
}
}
[self.navigationController popToViewController:targetVC animated:NO];
});
I am writing a simple application that has 3 view controllers. The root view controller is an item listing, basic table view. Off of this view controller, I push two different view controllers based on some user interaction - a create item view controller or a view item view controller.
So, the storyboard segues just look like a V, or something.
On my create item view controller, I would like it to pop back to the root view controller when the user creates a new item, but then push to the view item controller so that I can look at the newly created item.
I can't seem to get this to work. It's easy enough to pop back to the root view controller, but I'm unable to push that view item controller.
Any ideas? I've pasted my code, below. The pop function works, but the new view never appears.
- (void) onSave:(id)sender {
CLLocation *currentLocation = [[LocationHelper sharedInstance] currentLocation];
// format the thread object dictionary
NSArray* location = #[ #(currentLocation.coordinate.latitude), #(currentLocation.coordinate.longitude) ];
NSDictionary* thread = #{ #"title": _titleField.text, #"text": _textField.text, #"author": #"mustached-bear", #"location": location };
// send the new thread to the api server
[[DerpHipsterAPIClient sharedClient] postPath:#"/api/thread"
parameters:thread
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// init thread object
Thread *thread = [[Thread alloc] initWithDictionary:responseObject];
// init view thread controller
ThreadViewController *viewThreadController = [[ThreadViewController alloc] init];
viewThreadController.thread = thread;
[self.navigationController popToRootViewControllerAnimated:NO];
[self.navigationController pushViewController:viewThreadController animated:YES];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self.navigationController popToRootViewControllerAnimated:YES];
}];
}
If I understand you correctly, you have a stack of view controllers:
A (root) - B - C - D - E
And you want it to become:
A (root) - F
Right? In that case:
NSArray *viewControllers = self.navigationController.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray array];
// preserve the root view controller
[newViewControllers addObject:[viewControllers objectAtIndex:0]];
// add the new view controller
[newViewControllers addObject:viewThreadController];
// animatedly change the navigation stack
[self.navigationController setViewControllers:newViewControllers animated:YES];
Swift 4
// get current view controllers in stack and replace them
let viewControllers = self.navigationController!.viewControllers
let newViewControllers = NSMutableArray()
// preserve the root view controller
newViewControllers.add(viewControllers[0])
// add the new view controller
newViewControllers.add(viewThreadController)
// animatedly change the navigation stack
self.navigationController?.setViewControllers(newViewControllers as! [UIViewController], animated: true)
I think
[self.navigationController pushViewController:viewThreadController animated:YES];
is using a different NavigationController than the statement before that.
Because after popping to the root view Controller you loose the navigation Controller you are in. Solve that using this code instead
UINavigationController *nav = self.navigationController;
[self.navigationController popToRootViewControllerAnimated:NO];
[nav pushViewController:viewThreadController animated:YES];
I also think that this wont solve your whole problem. You will probably get an error saying that two fast popping and pushing may invalidate the NavigationController.
And to solve that you can either push the NavigationController in the viewDidDissappear Method of the 2nd View Controller or push it in the viewDidAppear Method in the Main View Controller(item listing).
An easy way to accomplish what you want to do is to build some simple logic into your main root view controllers -(void)viewWillAppear method and use a delegate callback to flip the logic switch. basically a "back reference" to the root controller. here is a quick example.
main root controller (consider this controller a) - well call it controllerA
set a property to keep track of the jump status
#property (nonatomic) BOOL jumpNeeded;
setup some logic in
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.jumpNeeded ? NSLog(#"jump needed") : NSLog(#"no jump needed");
if (self.jumpNeeded) {
NSLog(#"jumping");
self.jumpNeeded = NO;
[self performSegueWithIdentifier:#"controllerC" sender:self];
}
}
Now, in your main root controller,when a tableview row is selected do something like this
when pushing to controllerB in your tableView did select method
[self performSegueWithIdentifer#"controllerB" sender:self];
then implement your prepare for segue method
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
//setup controller B
if([segue.identifier isEqualTo:#"controllerB"]){
ControllerB *b = segue.destinationViewController;
b.delegate = self; //note this is the back reference
}
//implement controller c here if needed
}
Now move on to controllerB
you need to set a property called "delegate" to hold the back reference
and you need to import the header file from the root controller
#import "controllerA"
#property (nonatomic,weak) controllerA *delegate;
then just before you pop back to controllerA, you set the flag
self.delegate.jumpNeeded = YES;
[self.navigationController popViewControllerAnimated:YES];
and that is about it. You don't have to do anything with controllerC. There are a few other ways to do, but this is pretty straight forward for your needs. hope it works out for you.
Sharing a category on UINavigationController based on Dave DeLong's answer that we use in our application to keep the back button always working as required.
#implementation UINavigationController (PushNewAndClearNavigationStackToParent)
- (void) pushNewControllerAndClearStackToParent:(UIViewController*)newCont animated:(BOOL)animated
{
NSArray *viewControllers = self.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray array];
// preserve the root view controller
[newViewControllers addObject:[viewControllers firstObject]];
// add the new view controller
[newViewControllers addObject:newCont];
// animatedly change the navigation stack
[self setViewControllers:newViewControllers animated:animated];
}
#end
NSArray *viewControllers = self.navigationController.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray array];
// preserve the root view controller
for(int i = 0; i < [viewControllers count];i++){
if(i != [viewControllers count]-1)
[newViewControllers addObject:[viewControllers objectAtIndex:i]];
}
// add the new view controller
[newViewControllers addObject:conversationViewController];
// animatedly change the navigation stack
[self.navigationController setViewControllers:newViewControllers animated:YES];
I don't think this is possible because popping back will deallocate everything.
I think a better way is to put your data on a model singleton class, pop to the rootviewcontroller and listen for the pop to end. Then you check if there is some data stored on the model so that you know if you should push a new viewcontroller.
I used Dave's answer as inspiration and made this for Swift:
let newViewControllers = NSMutableArray()
newViewControllers.addObject(HomeViewController())
newViewControllers.addObject(GroupViewController())
let swiftArray = NSArray(array:newViewControllers) as! Array<UIViewController>
self.navigationController?.setViewControllers(swiftArray, animated: true)
Replace HomeViewController() with whatever you want your bottom view controller to be
Replace GroupViewController() with whatever you want your top view controller to be
If what you want to do is navigate in any way inside the same navigation controller... you can access the navigationStack A.K.A. viewControllers and then set the viewcontrollers in the order that you want, or add or remove some...
This is the cleanest and native way to do it.
UINavigationController* theNav = viewControllerToDismiss.navigationController;
UIViewController* theNewController = [UIViewController new];
UIViewController* rootView = theNav.viewControllers[0];
[theNav setViewControllers:#[
rootView,
theNewController
] animated:YES];
do any necessary validations so you don't get any exception.
I am getting the following error whenever I try to switch views like this:
-(void)changeView1ToView4 {
[self.navigationController1 pushViewController:view4 animated:YES];
}
This doesn't happen when the app first loads and user goes directly to this view. This crash only happens when I go to one of my other views, come back to the main menu, and then try to go to this view.
Also if you weren't sure already, I am using a UINavigationController. Also this code is in the app delegate and I am calling it from a view controller which has a parent view so I am using a .reference to call it like this:
[self.reference changeView1ToView4];
Is there any real way to fix this?
Thanks!
Edit1:
[self.navigationController1 pushViewController:view4 animated:NO];
[self.navigationController1 pushViewController:view4 animated:YES];
I tried that and got this crash message in the console:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing the same view controller instance more than once is not supported (<View2: 0x18d540>)'
When pushing 2 views onto the stack, try to call:
[self.navigationController1 pushViewController:view4 animated:YES];
and
[self.navigationController1 pushViewController://secondviewcontrollername// animated:NO];
If you try to push more than one view with the animated: field set to YES on both, then you confuse the stack. Only animate one view at a time.
just an FYI, if you call setViewControllers:animated: there's no need to call pushViewController: afterwards, else you'll get the "Pushing the same view controller instance more than once is not supported" crash.
#try {
[self.navController pushViewController:viewController animated:NO];
} #catch (NSException * e) {
NSLog(#"Exception: %#", e);
[self.navigationController popToViewController:viewController animated:NO];
} #finally {
//NSLog(#"finally");
}
Check this condition before push
if (![[self.navigationController topViewController] isKindOfClass:[YOURCONTROLLER class]])
[self.navigationController pushViewController:YOURCONTROLLER animated:YES];