Dismiss more than one view controller simultaneously

40,703

Solution 1

@Ken Toh's comment was what worked for me in this situation -- call dismiss from the view controller that you want to show after everything else is dismissed.

If you have a "stack" of 3 presented view controllers A, B and C, where C is on top, then calling A.dismiss(animated: true, completion: nil) will dismiss B and C simultaneously.

If you don't have a reference to the root of the stack, you could chain a couple of accesses to presentingViewController to get to it. Something like this:

self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)

Solution 2

You can dismiss WinVC's presenting controller (GameViewController) in the completion block:

let presentingViewController = self.presentingViewController
self.dismissViewControllerAnimated(false, completion: {
  presentingViewController?.dismissViewControllerAnimated(true, completion: {})
})

Alternatively, you could reach out to the root view controller and call dismissViewControllerAnimated, which will dismiss both modal viewcontrollers in a single animation:

self.presentingViewController?.presentingViewController?.dismissViewControllerAnimated(true, completion: {})

Solution 3

Swift 5 (and possibly 4, 3 etc)

presentingViewController?.presentingViewController? is not very elegant and doesn't work in some instances. Instead, use segues.

Let's say that we have ViewControllerA, ViewControllerB, and ViewControllerC. We are at ViewControllerC (we landed here through ViewControllerA -> ViewControllerB, so if we do dismiss we will go back to ViewControllerB). We want from ViewControllerC to jump straight back to ViewControllerA.

In ViewControllerA add the following action in your ViewController class:

@IBAction func unwindToViewControllerA(segue: UIStoryboardSegue) {}

Yes, this line goes in the ViewController of the ViewController you want to go back to!

Now, you need to create an exit segue from the ViewControllerC's storyboard (StoryboardC). Go ahead and open StoryboardC and select the storyboard. Hold CTRL down and drag to exit as follows:

enter image description here

You will be given a list of segues to choose from including the one we just created:

enter image description here

You should now have a segue, click on it:

enter image description here

Go in the inspector and set a unique id: enter image description here

In the ViewControllerC at the point where you want to dismiss and return back to ViewControllerA, do the following (with the id we set in the inspector previously):

self.performSegue(withIdentifier: "yourIdHere", sender: self)

Solution 4

You should be able to call:

self.presentingViewController.dismissViewControllerAnimated(true, completion: {});

(You may need to add ? or ! somewhere - I'm not a swift developer)

Solution 5

There's special unwind segue intended to roll back view stack to certain view controller. Please see my answer here: how to dismiss 2 view controller in swift ios?

Share:
40,703

Related videos on Youtube

codeinjuice
Author by

codeinjuice

Updated on September 07, 2021

Comments

  • codeinjuice
    codeinjuice over 2 years

    I'm making a game using SpriteKit. I have 3 viewControllers: selecting level vc, game vc, and win vc. After the game is over, I want to show the win vc, then if I press OK button on the win vc, I want to dismiss the win vc AND the game vc (pop two view controllers out of the stack). But I don't know how to do it because if I call

    self.dismissViewControllerAnimated(true, completion: {})    
    

    the win vc (top of the stack) is dismissed, so I don't know where to call it again to dismiss the game vc. Is there any way I can fix this without using navigation controller?

    This is the 1st VC: (Please pay attention to my comments below starting with "//")

    class SelectLevelViewController: UIViewController { // I implemented a UIButton on its storyboard, and its segue shows GameViewController
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    }
    

    This is the 2nd VC:

    class GameViewController: UIViewController, UIPopoverPresentationControllerDelegate {
        var scene: GameScene!
        var stage: Stage!
    
        var startTime = NSTimeInterval()
        var timer = NSTimer()
        var seconds: Double = 0
        var timeStopped = false
    
        var score = 0
    
        @IBOutlet weak var targetLabel: UILabel!
        @IBOutlet var displayTimeLabel: UILabel!
        @IBOutlet weak var scoreLabel: UILabel!
        @IBOutlet weak var gameOverPanel: UIImageView!
        @IBOutlet weak var shuffleButton: UIButton!
        @IBOutlet weak var msNum: UILabel!
    
        var mapNum = Int()
        var stageNum = Int()
    
        var tapGestureRecognizer: UITapGestureRecognizer!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let skView = view as! SKView
            skView.multipleTouchEnabled = false
    
            scene = GameScene(size: skView.bounds.size)
            scene.scaleMode = .AspectFill
            msNum.text = "\(mapNum) - \(stageNum)"
    
            stage = Stage(filename: "Map_0_Stage_1")
            scene.stage = stage
            scene.addTiles()
            scene.swipeHandler = handleSwipe
    
            gameOverPanel.hidden = true
            shuffleButton.hidden = true
    
            skView.presentScene(scene)
    
            Sound.backgroundMusic.play()
    
            beginGame()
        }
    
        func beginGame() {
            displayTimeLabel.text = String(format: "%ld", stage.maximumTime)
            score = 0
            updateLabels()
    
            stage.resetComboMultiplier()
    
            scene.animateBeginGame() {
                self.shuffleButton.hidden = false
            }
    
            shuffle()
    
            startTiming()
        }
    
        func showWin() {
            gameOverPanel.hidden = false
            scene.userInteractionEnabled = false
            shuffleButton.hidden = true
    
            scene.animateGameOver() {
                self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideWin")
                self.view.addGestureRecognizer(self.tapGestureRecognizer)
            }
        }
    
        func hideWin() {
            view.removeGestureRecognizer(tapGestureRecognizer)
            tapGestureRecognizer = nil
    
            gameOverPanel.hidden = true
            scene.userInteractionEnabled = true
    
            self.performSegueWithIdentifier("win", sender: self) // this segue shows WinVC but idk where to dismiss this GameVC after WinVC gets dismissed...
        }
    
        func shuffle() {...}
        func startTiming() {...}
    }
    

    And this is the 3rd VC:

    class WinVC: UIViewController {
    
        @IBOutlet weak var awardResult: UILabel!
    
        @IBAction func dismissVC(sender: UIButton) {
            self.dismissViewControllerAnimated(true, completion: {}) // dismissing WinVC here when this button is clicked
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
    
    }
    
    • Ramis
      Ramis almost 7 years
      More generic way to dismiss more that one modal view controllers is here
    • Eric
      Eric over 2 years
      Most of the answers below contain an animation glitch (briefly showing the intermediate vc during dismissal). Thankfully, I was able to solve it with this simple solution.
  • Gruntcakes
    Gruntcakes almost 9 years
    How does this dismiss multiple view controllers?
  • codeinjuice
    codeinjuice almost 9 years
    It worked! I added: self.dismissViewControllerAnimated(true, completion: {}) self.presentingViewController?.dismissViewControllerAnimated‌​(true, completion: {})
  • codeinjuice
    codeinjuice almost 9 years
    Did not work... it only dismisses the vc on the top of stack
  • Ken Toh
    Ken Toh almost 9 years
    Now reading your question again, it's not clear if your viewcontrollers (gamevc and winvc) are presented modally, or pushed. Are you using a navigation controller?
  • Nils Ziehn
    Nils Ziehn almost 9 years
    I srsly don't understand the downvotes. This will actually do what the OP asks.
  • Nils Ziehn
    Nils Ziehn almost 9 years
    The reason why it's not working is because self.presentingViewController is nilled in completion
  • Ken Toh
    Ken Toh almost 9 years
    Nils you are right, it's nilled. One way is capture the presentingViewController in a variable before hand. Just tested this and it should work.
  • Ken Toh
    Ken Toh almost 9 years
    You'll have to go up one more level to be able to dismiss both modal view controllers. If I am not mistaken, dismissViewControllerAnimated works by dismissing it's immediate child view controller and everything above the stack. It only dismisses itself if its the currently presented view controller (for which it delegates the message to its presenting view controller). See developer.apple.com/library/ios/documentation/UIKit/Referenc‌​e/…:
  • codeinjuice
    codeinjuice almost 9 years
    @kentoh I think you are right... There seems to be a memory leak even if the game VC has been dismissed. Then how can I dismiss the gameVC itself, not just a child of it? Would delegation be the only way? (p.s. every segue is performed as a "show", I'm not sure if that is modal or push...)
  • crazy_phage
    crazy_phage over 8 years
    @minsanity is right, I changed a little bit code here.
  • shim
    shim almost 6 years
    Any way to do this without the intermediate view controller "B" showing briefly during the transition?
  • axunic
    axunic over 5 years
    controller B still showing for a split second when transitioning to A. is there workaround to this problem?
  • Phlippie Bosman
    Phlippie Bosman over 5 years
    @mnemonic23 I agree, the other VC is momentarily visible. Unfortunately I don't have a workaround.
  • A.Kant
    A.Kant about 5 years
    I did like you, but I corrected a little your code for three VK (one root and two modal): if let first = presentingViewController, let second = first.presentingViewController { first.view.isHidden = true first.dismiss (animated: true) { second.dismiss (animated: true, completion: nil) } } P.S. The previous answers did not help me. Swift 4.2
  • Anton Eregin
    Anton Eregin over 4 years
    Amazing! Works for me in Swift 4)
  • Phontaine Judd
    Phontaine Judd over 4 years
    Unfortunately, this doesn't work when the VCs are presented modally. It shows multiple dismissal animations. Weird.
  • iOS_Mouse
    iOS_Mouse over 3 years
    This is the way. And here's another tutorial link that might help medium.com/@mimicatcodes/…
  • Zachary Nicoll
    Zachary Nicoll about 3 years
    If you're using storyboards and segues, this is gold. Thankyamuch.
  • Eric
    Eric over 2 years
    This answer is a good workaround, but it comes with a new (but less annoying) visual problem. You can't animate the dismissal unless you're okay with a black screen for a split second.
  • Eric
    Eric over 2 years
    See my answer below for an easy solution without the animation glitch.
  • Eric
    Eric over 2 years
    See my answer below for an easy solution without the animation glitch.
  • Eric
    Eric over 2 years
    This answer is a good workaround, but it comes with a new (but less annoying) visual problem. You can't animate the dismissal unless you're okay with a black screen for a split second. However, I was able to figure out an easy solution that solves the animation glitch completely!