简体   繁体   中英

Replace a UIViewController in the UINavigationController hierarchy

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

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.

NSMutableArray *viewController = [NSMutableArray arrayWithArray:[navController viewControllers]];
[viewController replaceObjectAtIndex:1 withObject:replacementController];
[navController setViewControllers:viewController];

See the UINavigationController class reference for more information.

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)
  }
}

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];
}

Swift 4 version:

if var viewControllers = navigationController?.viewControllers {
    viewControllers[viewControllers.count - 1] = newViewController
    navigationController?.viewControllers = viewControllers
}

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

- (void)swapTopViewController:(UIViewController *)topViewController{
    NSArray *viewControllers = [self.navigationController viewControllers];
    NSMutableArray *editableViewControllers = [NSMutableArray arrayWithArray:viewControllers];
    [editableViewControllers removeLastObject];
    [editableViewControllers addObject:topViewController];
    [self.navigationController setViewControllers:editableViewControllers];
}
extension UINavigationController {

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

}

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)
        }
    }
}

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)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM