113

I have a navigation stack, with say 5 UIViewControllers. I want to remove the 3rd and 4th viewcontrollers in the stack on the click of a button in the 5th viewcontroller. Is it possible to do this? If so how?

Jean Paul
  • 2,389
  • 5
  • 27
  • 37

16 Answers16

187

Use this code and enjoy:

NSMutableArray *navigationArray = [[NSMutableArray alloc] initWithArray: self.navigationController.viewControllers];

// [navigationArray removeAllObjects];    // This is just for remove all view controller from navigation stack.
[navigationArray removeObjectAtIndex: 2];  // You can pass your index here
self.navigationController.viewControllers = navigationArray;
[navigationArray release];

Hope this will help you.

Edit: Swift Code

guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all UIViewController stack as Array
navigationArray.remove(at: navigationArray.count - 2) // To remove previous UIViewController
self.navigationController?.viewControllers = navigationArray

Edit: To remove all ViewController except last one -> no Back Button in the upper left corner

guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all UIViewController stack as Array
let temp = navigationArray.last
navigationArray.removeAll()
navigationArray.append(temp!) //To remove all previous UIViewController except the last one
self.navigationController?.viewControllers = navigationArray
Community
  • 1
  • 1
Nitin
  • 7,455
  • 2
  • 32
  • 51
53

You can first get all the view controllers in the array and then after checking with the corresponding view controller class, you can delete the one you want.

Here is small piece of code:

NSArray* tempVCA = [self.navigationController viewControllers];

for(UIViewController *tempVC in tempVCA)
{
    if([tempVC isKindOfClass:[urViewControllerClass class]])
    {
        [tempVC removeFromParentViewController];
    }
}

I think this will make your work easier.

Adam Johns
  • 35,397
  • 25
  • 123
  • 176
Sourabh Bhardwaj
  • 2,524
  • 1
  • 17
  • 19
  • This one can be used for multi purpose. Thanks :) – Hemang Aug 21 '13 at 06:53
  • 10
    When I use this the controller is removed properly. But when I use the "Back" button my navigation bar shows the information of the removed viewController. Does anybody else receive this weird behavior and how can I fix it? – Robin Ellerkmann Sep 08 '15 at 07:32
  • Removing a the viewcontroller from parentviewcontroller? It seemed dodgy to me so I tested on iOS 9 and it simply removes view of tempVC from itself and returns it to its default state as in storyboard. Thats about it – NSNoob Dec 03 '15 at 09:25
  • 1
    @Robin Ellerkmann did you find solution for that problem? i am removing viewcontroller but back button remains at navigation bar. – Mehmet Emre Portakal Feb 26 '16 at 15:38
  • 2
    @MehmetEmre I use Swift 2.1 with self.navigationController?.viewControllers.removeLast(). This works pretty good for me. – Robin Ellerkmann Feb 26 '16 at 15:51
  • great. swift gives better control :) – Sourabh Bhardwaj Feb 29 '16 at 07:44
  • 2
    When I was in 4 viewcontroller memory was 80MB when log out all viewcontroller get removed. Memory still 80MB. So memory is not releasing. :( – Anil Gupta Mar 09 '17 at 14:45
  • My back button stops working.....I have NavController, vc1, vc2, vc3. If I'm on vc3 and do this strategy to remove vc2, now my back button won't work. I was hoping I could just press it to go back to vc1 but it doesn't do anything... – Eric Mar 08 '22 at 02:47
43

Swift 3 & 4/5

self.navigationController!.viewControllers.removeAll()

self.navigationController?.viewControllers.remove(at: "insert here a number")

Swift 2.1

remove all:

self.navigationController!.viewControllers.removeAll()

remove at index

self.navigationController?.viewControllers.removeAtIndex("insert here a number")

There a bunch of more possible actions like removeFirst,range etc.

kuzdu
  • 7,124
  • 1
  • 51
  • 69
27

Swift 5:

navigationController?.viewControllers.removeAll(where: { (vc) -> Bool in
    if vc.isKind(of: MyViewController.self) || vc.isKind(of: MyViewController2.self) {
        return false
    } else {
        return true
    }
})
Amit
  • 4,837
  • 5
  • 31
  • 46
Niklas
  • 1,322
  • 14
  • 11
  • 8
    `return !vc.isKind(of: MyViewController.self) && !vc.isKind(of: MyViewController2.self)` would do the job in one line :-) – Mark Jun 20 '19 at 13:00
18

Swift 5, Xcode 13

I found this approach simple by specifying which view controller(s) you want to remove from the navigation stack.

extension UINavigationController {
    
    func removeViewController(_ controller: UIViewController.Type) {
        if let viewController = viewControllers.first(where: { $0.isKind(of: controller.self) }) {
            viewController.removeFromParent()
        }
    }
}

Example use:

navigationController.removeViewController(YourViewController.self)
Mitchell C
  • 309
  • 3
  • 9
12

Using setViewControllers function from UINavigationController is the best way. There is also animated parameter to enable animation.

func setViewControllers(_ viewControllers: [UIViewController], animated: Bool)

Example in swift for question

func goToFifthVC() {

    var currentVCStack = self.navigationController?.viewControllers
    currentVCStack?.removeSubrange(2...3)

    let fifthVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "fifthVC")
    currentVCStack?.append(fifthVC)

    self.navigationController?.setViewControllers(currentVCStack!, animated: true)
}

I tried other ways like [tempVC removeFromParentViewController];. It make weird behaviour, removed ViewController navigation still showing when pop back like reported by @robin-ellerkmann

Thein
  • 3,940
  • 2
  • 30
  • 34
  • 5
    This is actually the best solution: removing the VC from the navigationController?.viewControllers array and using setViewControllers to assign the new array. I've also checked for zombies or reference cycles, it's safe. – OhadM Nov 04 '18 at 12:40
  • I confirm that it's an excellent solution: I'm actually using that `setViewControllers(_:animated:)` technique in both ways: to pop multiple controllers and to push multiple controllers. – Cœur Feb 03 '19 at 17:06
8

Swift 2.0:

  var navArray:Array = (self.navigationController?.viewControllers)!
  navArray.removeAtIndex(navArray.count-2)
  self.navigationController?.viewControllers = navArray
tahir raees
  • 89
  • 1
  • 4
  • 2
    So you're not force unwrapping the navigation controller, you could make it an if statement `if var navArray = ... { ... }` – Kiley Aug 11 '16 at 15:11
7

Details

  • Swift 5.1, Xcode 11.3.1

Solution

extension UIViewController {
    func removeFromNavigationController() { navigationController?.removeController(.last) { self == $0 } }
}

extension UINavigationController {
    enum ViewControllerPosition { case first, last }
    enum ViewControllersGroupPosition { case first, last, all }

    func removeController(_ position: ViewControllerPosition, animated: Bool = true,
                          where closure: (UIViewController) -> Bool) {
        var index: Int?
        switch position {
            case .first: index = viewControllers.firstIndex(where: closure)
            case .last: index = viewControllers.lastIndex(where: closure)
        }
        if let index = index { removeControllers(animated: animated, in: Range(index...index)) }
    }

    func removeControllers(_ position: ViewControllersGroupPosition, animated: Bool = true,
                           where closure: (UIViewController) -> Bool) {
        var range: Range<Int>?
        switch position {
            case .first: range = viewControllers.firstRange(where: closure)
            case .last:
                guard let _range = viewControllers.reversed().firstRange(where: closure) else { return }
                let count = viewControllers.count - 1
                range = .init(uncheckedBounds: (lower: count - _range.min()!, upper: count - _range.max()!))
            case .all:
                let viewControllers = self.viewControllers.filter { !closure($0) }
                setViewControllers(viewControllers, animated: animated)
                return
        }
        if let range = range { removeControllers(animated: animated, in: range) }
    }

    func removeControllers(animated: Bool = true, in range: Range<Int>) {
        var viewControllers = self.viewControllers
        viewControllers.removeSubrange(range)
        setViewControllers(viewControllers, animated: animated)
    }

    func removeControllers(animated: Bool = true, in range: ClosedRange<Int>) {
        removeControllers(animated: animated, in: Range(range))
    }
}

private extension Array {
    func firstRange(where closure: (Element) -> Bool) -> Range<Int>? {
        guard var index = firstIndex(where: closure) else { return nil }
        var indexes = [Int]()
        while index < count && closure(self[index]) {
            indexes.append(index)
            index += 1
        }
        if indexes.isEmpty { return nil }
        return Range<Int>(indexes.min()!...indexes.max()!)
    }
}

Usage

removeFromParent()

navigationController?.removeControllers(in: 1...3)

navigationController?.removeController(.first) { $0 != self }

navigationController?.removeController(.last) { $0 != self }

navigationController?.removeControllers(.all) { $0.isKind(of: ViewController.self) }

navigationController?.removeControllers(.first) { !$0.isKind(of: ViewController.self) }

navigationController?.removeControllers(.last) { $0 != self }

Full Sample

Do not forget to paste here the solution code

import UIKit

class ViewController2: ViewController {}

class ViewController: UIViewController {

    private var tag: Int = 0
    deinit { print("____ DEINITED: \(self), tag: \(tag)" ) }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("____ INITED: \(self)")
        let stackView = UIStackView()
        stackView.axis = .vertical
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true

        stackView.addArrangedSubview(createButton(text: "Push ViewController() white", selector: #selector(pushWhiteViewController)))
        stackView.addArrangedSubview(createButton(text: "Push ViewController() gray", selector: #selector(pushGrayViewController)))
        stackView.addArrangedSubview(createButton(text: "Push ViewController2() green", selector: #selector(pushController2)))
        stackView.addArrangedSubview(createButton(text: "Push & remove previous VC", selector: #selector(pushViewControllerAndRemovePrevious)))
        stackView.addArrangedSubview(createButton(text: "Remove first gray VC", selector: #selector(dropFirstGrayViewController)))
        stackView.addArrangedSubview(createButton(text: "Remove last gray VC", selector: #selector(dropLastGrayViewController)))
        stackView.addArrangedSubview(createButton(text: "Remove all gray VCs", selector: #selector(removeAllGrayViewControllers)))
        stackView.addArrangedSubview(createButton(text: "Remove all VCs exept Last", selector: #selector(removeAllViewControllersExeptLast)))
        stackView.addArrangedSubview(createButton(text: "Remove all exept first and last VCs", selector: #selector(removeAllViewControllersExeptFirstAndLast)))
        stackView.addArrangedSubview(createButton(text: "Remove all ViewController2()", selector: #selector(removeAllViewControllers2)))
        stackView.addArrangedSubview(createButton(text: "Remove first VCs where bg != .gray", selector: #selector(dropFirstViewControllers)))
        stackView.addArrangedSubview(createButton(text: "Remove last VCs where bg == .gray", selector: #selector(dropLastViewControllers)))
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if title?.isEmpty ?? true { title = "First" }
    }

    private func createButton(text: String, selector: Selector) -> UIButton {
        let button = UIButton()
        button.setTitle(text, for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: selector, for: .touchUpInside)
        return button
    }
}

extension ViewController {

    private func createViewController<VC: ViewController>(backgroundColor: UIColor = .white) -> VC {
        let viewController = VC()
        let counter = (navigationController?.viewControllers.count ?? -1 ) + 1
        viewController.tag = counter
        viewController.title = "Controller \(counter)"
        viewController.view.backgroundColor = backgroundColor
        return viewController
    }

    @objc func pushWhiteViewController() {
        navigationController?.pushViewController(createViewController(), animated: true)
    }

    @objc func pushGrayViewController() {
        navigationController?.pushViewController(createViewController(backgroundColor: .lightGray), animated: true)
    }

    @objc func pushController2() {
        navigationController?.pushViewController(createViewController(backgroundColor: .green) as ViewController2, animated: true)
    }

    @objc func pushViewControllerAndRemovePrevious() {
        navigationController?.pushViewController(createViewController(), animated: true)
        removeFromNavigationController()
    }

    @objc func removeAllGrayViewControllers() {
        navigationController?.removeControllers(.all) { $0.view.backgroundColor == .lightGray }
    }

    @objc func removeAllViewControllersExeptLast() {
        navigationController?.removeControllers(.all) { $0 != self }
    }

    @objc func removeAllViewControllersExeptFirstAndLast() {
        guard let navigationController = navigationController, navigationController.viewControllers.count > 1 else { return }
        let lastIndex = navigationController.viewControllers.count - 1
        navigationController.removeControllers(in: 1..<lastIndex)
    }

    @objc func removeAllViewControllers2() {
        navigationController?.removeControllers(.all) { $0.isKind(of: ViewController2.self) }
    }

    @objc func dropFirstViewControllers() {
        navigationController?.removeControllers(.first) { $0.view.backgroundColor != .lightGray }
    }

    @objc func dropLastViewControllers() {
        navigationController?.removeControllers(.last) { $0.view.backgroundColor == .lightGray }
    }

    @objc func dropFirstGrayViewController() {
        navigationController?.removeController(.first) { $0.view.backgroundColor == .lightGray }
    }

    @objc func dropLastGrayViewController() {
        navigationController?.removeController(.last) { $0.view.backgroundColor == .lightGray }
    }
}

Result

enter image description here

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
6

If you are trying to move to 2nd view controller from 5th view controller (skipping 3rd and 4th), you would like to use [self.navigationController popToviewController:secondViewController].

You can obtain the secondViewController from the navigation controller stack.

secondViewController =  [self.navigationController.viewControllers objectAtIndex:yourViewControllerIndex];
Vignesh
  • 10,205
  • 2
  • 35
  • 73
  • 1
    Dont want to pop the current viewcontroller. The current viewcontroller should remain intact. But I need to pop the 2 viewcontrollers lying under it in the stack – Jean Paul Apr 23 '12 at 13:55
  • @JeanPaulScott. I wonder Why would you want to do that, if not for popping up?!. – Vignesh Apr 24 '12 at 04:27
  • There is a case where I would have different instances of the same viewcontroller being pushed into the stack. So when a new instance is created and pushed into the stack, I want to pop out the previous instance and the viewcontroller associated with that. – Jean Paul Apr 24 '12 at 05:03
  • @Vignesh This wouldn't work as required in iOS 7 because of 'swipe to pop' gesture – Dennis Pashkov Feb 11 '14 at 11:35
  • @JeanPaulScott to achieve what you want, the safest thing is to pop twice before pushing your new view controller instance. – Radu Simionescu Sep 15 '15 at 09:37
4

Use this

if let navVCsCount = navigationController?.viewControllers.count {
    navigationController?.viewControllers.removeSubrange(Range(2..<navVCsCount - 1))
}

It will take care of ViewControllers of navigationController. viewControllers and also a navigationItems stacked in navigationBar.

Note: Be sure to call it at least after viewDidAppear

Nikola Markovic
  • 301
  • 1
  • 13
  • 1
    This method worked perfectly for me in Swift 5, Xcode 10.3...if let navVCsCount = navigationController?.viewControllers.count { self.navigationController?.viewControllers.removeSubrange(navVCsCount-3.. – Kedar Sukerkar Aug 02 '19 at 12:43
3

Swift 5.1, Xcode 11

extension UINavigationController{
public func removePreviousController(total: Int){
    let totalViewControllers = self.viewControllers.count
    self.viewControllers.removeSubrange(totalViewControllers-total..<totalViewControllers - 1)
}}

Make sure to call this utility function after viewDidDisappear() of previous controller or viewDidAppear() of new controller

Kedar Sukerkar
  • 1,410
  • 1
  • 16
  • 22
2

This solution worked for me in swift 4:

let VCCount = self.navigationController!.viewControllers.count
self.navigationController?.viewControllers.removeSubrange(Range(VCCount-3..<VCCount - 1))

your current view controller index in stack is:

self.navigationController!.viewControllers.count - 1
BabakHSL
  • 622
  • 1
  • 8
  • 18
1

Swift 5.4

removing SpecificViewController

navigationController.viewControllers.removeAll { $0 is SpecificViewController }
rami
  • 129
  • 2
  • 10
0

I wrote an extension with method which removes all controllers between root and top, unless specified otherwise.

extension UINavigationController {
func removeControllers(between start: UIViewController?, end: UIViewController?) {
    guard viewControllers.count > 1 else { return }
    let startIndex: Int
    if let start = start {
        guard let index = viewControllers.index(of: start) else {
            return
        }
        startIndex = index
    } else {
        startIndex = 0
    }

    let endIndex: Int
    if let end = end {
        guard let index = viewControllers.index(of: end) else {
            return
        }
        endIndex = index
    } else {
        endIndex = viewControllers.count - 1
    }
    let range = startIndex + 1 ..< endIndex
    viewControllers.removeSubrange(range)
}

}

If you want to use range (for example: 2 to 5) you can just use

    let range = 2 ..< 5
    viewControllers.removeSubrange(range)

Tested on iOS 12.2, Swift 5

Adam
  • 1,776
  • 1
  • 17
  • 28
0

// removing the viewcontrollers by class names from stack and then dismissing the current view.

 self.navigationController?.viewControllers.removeAll(where: { (vc) -> Bool in
      if vc.isKind(of: ViewController.self) || vc.isKind(of: ViewController2.self) 
       {
        return true
        } 
     else 
        {
         return false
         }
        })
self.navigationController?.popViewController(animated: false)
self.dismiss(animated: true, completion: nil)
Mirza Q Ali
  • 477
  • 5
  • 6
0

You should remove the controllers from navigation controller array and ALSO their parents

 navigationController?.viewControllers.removeAll(where: { (vc) -> Bool in
        if vc.isKind(of: MyViewController.self)
            || vc.isKind(of: MyViewControllerType2.self)
            || vc.isKind(of: MyViewControllerType3.self) {

           /* remove from parent */
            vc.removeFromParent()
            return true
        } else {
            return false
        }
    })
jamal zare
  • 1,037
  • 12
  • 13