Presenting a new view controller without infinitely stacking them? - ios

I am working on an app where it is kind of like a website. There is a menu that every viewcontroller has in it and that needs to switch to the correct view.
There is no concept of back in my application. Thus, if I use UINavigation to push a view, I end up stacking up lots of views.
Same problem if I add the vc as a child or present it, you end up with hundreds of vcs after a while.
What is a way I can design this to safely have only 1 view at any given time?
Thanks

Like this.
Add this to all your controllers that have access to the navigation controller. When you want to push a new controller, instead of using pushViewController:animated you can use this. You can modify the code to take animated as a parameter aswell.
- (void)pushIfNotInStack:(UIViewController*)viewController
{
BOOL isInStack = NO;
NSMutableArray * vcStack = [self.navigationController.viewControllers mutableCopy];
for (NSInteger i = 1 ; i <vcStack.count; i++){
if ([viewController isKindOfClass:[[vcStack objectAtIndex:i]class]]) {
[vcStack replaceObjectAtIndex:i withObject:viewController];
isInStack = YES;
}
}
if(isInStack){
[self.navigationController setViewControllers:vcStack];
[self.navigationController popToViewController:viewController animated:YES];
} else {
[self.navigationController pushViewController:viewController animated:YES];
}
}

Use a tab bar view controller and hide the tab bar.
A tab view controller can have a number of children view controllers. You can add these children view controllers in storyboard or in code.
You can use your menu to choose which child view controller you want to display. The children view controllers are indexed from 0 to n - 1, where n is the number of view controllers. If you want to display the view controller at index i:
[self.tabBarController setSelectedIndex:i];
Don't forget to hide your tab bar in each child view controller's viewDidLoad

You can also sublass your UINavigationController class and override pushViewController:animated: method as follow:
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewControllers.count > <#the number of controllers you want to keep in memory eg. 20 #>) {
NSMutableArray *mutableViewControllers = [self.viewControllers mutableCopy];
//remove 2 last controllers in controller stack (from the bottom)
[mutableViewControllers removeObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
self.viewControllers = [mutableViewControllers copy];
}
[super pushViewController:viewController animated:animated];
}
This approach gurantees you to alloc init controllers in normal way and don't bother about they pointers to memory.

Related

pop To View Controller

I am new to coding objective-C and am trying to use pop to viewController. To do this when a button is tapped i use
[self.navigationController popToViewController:(what goes here) animated:YES];
I know its a UIViewController* that goes there but I am wondering where do I declare this and what code would I use to declare it.
My basic storyboard is I have 4 view controller A,B,C,D and A is the root which pushed to B which pushes to C which pushes to D, my button is on D and I am trying to pop back to B. The object at index method won't work because its doesn't always go A->B->C->D sometimes it goes A->C->B->D so
[self.navigationController popToViewController: [self.navigationController.viewControllers objectAtIndex:2] animated:YES];
So that doesn't work.
Thanks for the help in advance. Sorry if this question is too basic.
You need a way to find the desired view controller to pop to.
-(IBAction)popToDesiredViewController:(id)sender
{
UIViewController *desiredVC = nil;
// LOOK AT ALL VIEW CONTROLLERS IN NAVIGATION CONTROLLER
for (UIViewController *oneVC in self.navigationController.viewControllers) {
// CHECK IF THIS IS THE VIEW CONTROLLER YOU WANT
// change this to your logic
BOOL foundDesiredVC = [oneVC isKindOfClass: [SignInVC class]];
if (foundDesiredVC) {
desiredVC = oneVC;
break;
}
}
[self.navigationController popToViewController:desiredVC animated:YES];
}
Personally I don't find this as a basic question, even I got struck in such a situation. But I found a good solution by using “Mediator Design pattern” and develop my custom UINavigationController as a coordinator by combining the method:
- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated
and maintaining my own navigation stack.
A coordinator (your custom UINavigationController) has to have the control over your navigation, if your are having unpredictable way of navigation.

How to switch between view controllers and get rid of the previous one

In android, switching between activities, is fairly straightforward
you call
Intent intent = new Intent(this,NextActivity.class); <- define the next activity
startActivity(intent); <- start the next activity
finish(); < -get rid of the current activity
now in iOS i know how to do this:
UIViewController *nextviewcontroller = [[UIViewController alloc]initWithNibName:#"nextvc" bundle:nil];
[self presentViewcontroller:nextviewcontroller animated:YES completion:nil];
How do I get rid of the current view controller? so that currentviewcontroller dies after presenting nextviewcontroller ?
[self dismissViewController:YES]; doesnt seem to do the trick
the lifecycle methods viewWillDisappear and viewDidDisappear are called even if I don't call [self dismissViewController:YES];
i want "currentviewcontroller" to be removed from the memory, and from the viewcontroller stack, so that clicking "back" in "nextviewcontroller" will go to some thirdviewcontroller that was before currentviewcontroller
In iOS is different, since there's no concept of Activity and everything is more focused on the app itself (in Android you can mix activities from different apps). Therefore, there's no concept of "view controller stack".
The most similar concept is the "navigation stack" of navigation controllers, where you actually push and pop new view controller into some kind of linear navigation. A navigation bar is automatically created and populated with back buttons.
presentViewController will show your view controller modally upon the current one, but you can't thrash the presenting one since it's holding and containing ("defining context") the new one.
If you use a navigation controller for your navigation hierarchy (I don't know if you can), you can override the back button and use something like
UIViewController * prev = self.navigationController.viewControllers[self.navigationController.viewControllers.count -2 ]
[self.navigationController popToViewController:prev animated:YES]
With a modal view controller, you may try something like (I haven't tried but it may work)
[self.presentingViewController.navigationController popViewControllerAnimated:YES]
You should write one of these code into the target action of your close button.
iOS doesn't maintain a global stack of controllers in the way that Android does. Each app shows a controller at its root, and that one is responsible for showing the other controllers in the app. Controllers can display other controllers modally using presentViewcontroller:animated:completion: but the presenting controller remains underneath the presented one.
If your current controller is the root controller, then instead of using presentViewcontroller:animated:completion: you'd just do this:
self.view.window.rootViewController = nextViewController;
It's very common for the root controller to be a UINavigationController, which does manage a stack of controllers. If that is the case, and if your current controller is at the top of the stack, you'd do this:
[self.navigationController popViewControllerAnimated:NO];
[self.navigationController pushViewController:nextViewController animated:YES];
If your setup is different, you'd do something different; it's hard to say what without knowing more. But it's most likely that you'd be in the UINavigationController case.
In the viewDidAppear of your nextviewcontroller you could add :
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSArray *controllers = self.navigationController.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray arrayWithArray:controllers];
[newViewControllers removeObjectAtIndex:[controllers count]-2];
self.navigationController.viewControllers = newViewControllers;
}
There is nothing available like this in iOS but you can achieve it doing something like below
NSArray *viewControllers=[self.navigationController viewControllers];
NSMutableArray *newControllers=[[NSMutableArray alloc] init];
for(int i=[viewControllers indexOfObject:self];i<viewControllers.count;i++){
[newControllers addObject:[viewControllers objectAtIndex:i]];
}
[self.navigationController setViewControllers:[[NSArray alloc] initWithArray:newControllers]];
I have tried the method of storing all the view controllers in an array but it didn't work for me . When you try popViewController it will move to the View Controller which is last in the stack.
You can make 2 navigation controllers and switch between them and also switch between the view controllers of a particular Navigation Controller.
For eg.
You can switch between 2 Navigation Controller using the following code:
FirstNavController *fisrtView=[storyboard instantiateViewControllerWithIdentifier:#"firstnavcontroller"];
self.window.rootViewController = firstView;
}else{
SecondNavController *secondView=[storyboard instantiateViewControllerWithIdentifier:#"loginnavcontroller"];
self.window.rootViewController = secondView;
}
If your FirstNavController has 2 ViewControllers then you can switch between them using pushViewController
SecondViewController *sc = [self.storyboard instantiateViewControllerWithIdentifier:#"secondviewcontroller"];
[[self navigationController] pushViewController:sc animated:YES];
and popViewController
[self.navigationController popViewControllerAnimated:YES];

removeFromParentViewController removes all the view controller from the stack

In my app, I want to remove a view controller from the stack of view controllers and then navigate to a particular view controller. Say I have A,B,C,D,E view controller stack. I wish to remove E and then go to C. I use the following code
NSMutableArray *allViewControllers = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
for (UIViewController *aViewController in allViewControllers)
{
if ([aViewController isKindOfClass:[noNetworkViewController class]])
{
[aViewController removeFromParentViewController];
}
}
NSMutableArray *allbViewControllers = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
for (UIViewController *aViewController in allbViewControllers)
{
if ([aViewController isKindOfClass:[messageViewController class]])
{
[self.navigationController popToViewController:aViewController animated:NO];
}
}
The first for loop erases all the view controllers from the stack. Is there something I am doing wrong here. Please let me know.
You should not be mucking around with a navigation controller's stack of view controllers using removeFromParentViewController:. Ever. That is flat-out wrong.
Furthermore, there is no reason to loop through a navigation controller's stack of view controllers and popping them one by one. There are methods like popToViewController:animated: and popToRootViewController that let you pop to any arbitrary view controller in the stack with one call.
Have you verified that the parent view controller property is populated by the navigation controller? The parent view controller is normally used for UIViewControllerContainment protocol conformance. UINavigationController does not conform to that protocol, which makes me think you're not going to be able to remove it from the navigation controller by calling that method. You can use – setViewControllers:animated: on your navigationController to assign the controllers. Just use the array you created with all the viewControllers.
If you are using a pop, you don't have to remove the other view from it's parent view. You can just pop to a view controller that is in the view controller array and back two from the top of the stack.
I would give a code example, but i'm not on my dev computer. If nobody has answered with more detail before tomorrow, I will add some code.
-Edit - Here is the line of code that I use to pop back 2 view controllers. I'm sure this could be done better ways, but it works.
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:[self.navigationController.viewControllers count]-3] animated:YES];

How to pop back to root view controller but then push to a different view?

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.

How to go 2 views back in navigation based application

I have created a View based based application of 4 views. By using navigation controller I am changing the view. In my 3rd view one button is there. If I click on that button the app should come to the first view (2 views back).
I have used
[self.navigationController popViewControllerAnimated:YES];
[self.navigationController popViewControllerAnimated:YES];
This is not working. It's going to the previous page only.
for (UIViewController *controller in self.navigationController.viewControllers)
{
if ([controller isKindOfClass:[NeededViewController class]])
{
[self.navigationController popToViewController:controller
animated:YES];
break;
}
}
Try out this, and make change according your specification
-(void)goToMainCategoryView;
{
id object = nil;
for (UIViewController *viewControl in self.navigationController.viewControllers)
{
if(viewControl.view.tag == 0)
{
object = viewControl;
}
}
[self.navigationController popToViewController:object animated:YES];
}
Another easy root to select the UIViewController by index would be to use:
NSArray *viewsArray = [self.navigationController viewControllers];
UIViewController *chosenView = [viewsArray objectAtIndex:1];
[self.navigationController popToViewController:chosenView animated:YES];
chosenView would then be the second view in the navigation stack (position 1). If you had a large stack and wanted to go a specific view.
Use
popToRootViewControllerAnimated:
to go all the way back to the top view controller:
Documentation:
Pops all the view controllers on the stack except the root view controller and updates the display.
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
or
popToViewController:animated:
to go back to a particular view controller, supply the view controller you want to go to.
Documentation:
Pops view controllers until the specified view controller is at the top of the navigation stack.
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
Parameters
viewController
The view controller that you want to be at the top of the stack.
The same thing in swift 1.2 :: xcode:6.4
for controller: UIViewController in self.navigationController?.viewControllers as! [UIViewController] {
if controller.isKindOfClass(YourViewController) {
self.navigationController!.popToViewController(controller, animated: true)
}
}

Resources