35

I have an iPhone app which uses a standard implementation of UINavigationController for the app navigation.

I am trying to figure out a way to replace a view controller in the hierarchy. In other words, my app loads into a rootViewController and when the user presses a button, the app pushes to firstViewController. Then the user pushes another button to navigate the app to secondViewController. Again the use navigates down to another view controller, thirdViewController. However, I want the BackButton of the thirdViewController to pop back to firstViewController.

Essentially, when the user pushes to thirdViewController, I would like it to replace secondViewController in the navigation hierarchy.

Is this possible? I know it is using Three20, but I'm not in this case. Nevertheless, if it's possible in Three20, then it certainly should be using straight SDK calls. Does anyone have any thoughts?

Cheers, Brett

0x8badf00d
  • 6,331
  • 3
  • 33
  • 66
Brett
  • 10,971
  • 27
  • 114
  • 198
  • I would also look into Unwind Segues, they're useful for accomplishing this kind of thing. Awesome SO answer on them here: http://stackoverflow.com/questions/12561735/what-are-unwind-segues-for-and-how-do-you-use-them – jrisberg Jul 07 '15 at 20:26
  • In the given example, wouldn't you simply pop (that will get rid of the second one), and then push (you now have 1-3). Alternately, if it's a "different button", when you're on 1-2-3, simply pop twice and you'll be on 1. – Fattie Jan 05 '18 at 00:11

10 Answers10

58

Pretty Simple, when about to push the thirdViewController instead of doing a simple pushViewController do this:

NSArray * viewControllers = [self.navigationController viewControllers];
NSArray * newViewControllers = [NSArray arrayWithObjects:[viewControllers objectAtIndex:0], [viewControllers objectAtIndex:1], thirdController,nil];
[self.navigationController setViewControllers:newViewControllers];

where [viewControllers objectAtIndex:0] and [viewControllers objectAtIndex:1] are your rootViewController and your FirstViewController.

Shaheen Ghiassy
  • 6,630
  • 2
  • 36
  • 39
Ecarrion
  • 4,800
  • 1
  • 29
  • 44
23
NSMutableArray *viewController = [NSMutableArray arrayWithArray:[navController viewControllers]];
[viewController replaceObjectAtIndex:1 withObject:replacementController];
[navController setViewControllers:viewController];

See the UINavigationController class reference for more information.

Necreaux
  • 8,456
  • 7
  • 22
  • 43
JustSid
  • 24,711
  • 7
  • 72
  • 97
18

A cleaner way in Swift should be:

extension UINavigationController {
  func replaceTopViewController(with viewController: UIViewController, animated: Bool) {
    var vcs = viewControllers
    vcs[vcs.count - 1] = viewController
    setViewControllers(vcs, animated: animated)
  }
}
duan
  • 6,962
  • 3
  • 40
  • 62
4

It is not possible to animate the transition if you simply replace the viewController in the navigation controllers view controller array. I would recommend doing the following in the viewWillAppear method of the 3rd view controller.

-(void) viewWillAppear:(BOOL)animated
{
   NSArray *vCs=[[self navigationController] viewControllers];
   NSMutableArray *nvCs=nil;
   //remove the view controller before the current view controller
   nvCs=[[NSMutableArray alloc]initWithArray:vCs];
   [nvCs removeObjectAtIndex:([nvCs count]-2)];
   [[self navigationController] setViewControllers:nvCs];
   [super viewWillAppear:animated];
}
pb2q
  • 54,061
  • 17
  • 135
  • 139
Rohit Gupta
  • 637
  • 4
  • 7
3

Swift 4 version:

if var viewControllers = navigationController?.viewControllers {
    viewControllers[viewControllers.count - 1] = newViewController
    navigationController?.viewControllers = viewControllers
}
Skoua
  • 2,690
  • 3
  • 37
  • 44
2

Since you are just trying to pop twice from the ViewControllers stack, you can probably get the same result by calling

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated

from the back button of the thirdViewController

Ivano
  • 23
  • 3
2
- (void)swapTopViewController:(UIViewController *)topViewController{
    NSArray *viewControllers = [self.navigationController viewControllers];
    NSMutableArray *editableViewControllers = [NSMutableArray arrayWithArray:viewControllers];
    [editableViewControllers removeLastObject];
    [editableViewControllers addObject:topViewController];
    [self.navigationController setViewControllers:editableViewControllers];
}
Tunaki
  • 116,530
  • 39
  • 281
  • 370
Arun Kumar P
  • 714
  • 2
  • 10
  • 23
1
extension UINavigationController {

    func setTop(viewController: UIViewController) {
        viewControllers = [viewController]
    }

}
Elijah
  • 7,223
  • 2
  • 49
  • 49
1

Swift 5 UINavigationController extension, which supports Fade animation as well when replacing.

extension UINavigationController {
    func find(of type: UIViewController.Type) -> UIViewController? {
        for controller in self.viewControllers {
            if controller.isKind(of: type) {
                return controller
            }
        }
        return nil
    }
    
    func go(to controller: UIViewController, animated: Bool = true) {
        if let old = self.find(of: type(of: controller)) {
            self.popToViewController(old, animated: animated)
        }
        else {
            self.pushViewController(controller, animated: animated)
        }
    }
    
    fileprivate func prepareFade() {
        let transition: CATransition = CATransition()
        transition.duration = 0.3
        transition.type = CATransitionType.fade
        view.layer.add(transition, forKey: nil)
    }
    
    func replace(to controller: UIViewController, animated: Bool = true, fade: Bool = true) {
        guard let current = self.topViewController else {
            return
        }
        
        var controllers = self.viewControllers
        controllers.remove(current)
        controllers.append(controller)
        
        if fade {
            prepareFade()
            self.setViewControllers(controllers, animated: false)
        }
        else {
            self.setViewControllers(controllers, animated: animated)
        }
    }
}

extension Array where Element: Equatable {
    mutating func remove(_ element: Element) {
        if let index = self.firstIndex(of: element) {
            self.remove(at: index)
        }
    }
}
Li Jin
  • 953
  • 2
  • 8
  • 18
0

Often you need to clear the stack to your "Base", and put the new one on:

This answer from @duan is perfect and will literally replace the top item with a new item:

extension UINavigationController {
    func replaceTopViewController(with vc: UIViewController, animated: Bool) {
        var vcs = viewControllers
        vcs[vcs.count - 1] = vc // NB watch for bizarre zero size
        setViewControllers(vcs, animated: animated)
    }
}

However very often, you'll have a "Base" controller which you want to always be there.

If you want to clear everything safely to the Base and change to the new one (WITHOUT viewWillAppear running in Base), then:

extension UINavigationController {
    func popToRoot(andPushOnly: UIViewController) {
        var vcs = viewControllers
        while vcs.count > 1 { vcs.removeLast() }
        if vcs.count < 1 { return }
        vcs.append(andPushOnly)
        setViewControllers(vcs, animated: false)
    }
}
Fattie
  • 30,632
  • 54
  • 336
  • 607