50

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

}
James Z
  • 12,209
  • 10
  • 24
  • 44
codeinjuice
  • 664
  • 1
  • 5
  • 14
  • More generic way to dismiss more that one modal view controllers is [here](https://stackoverflow.com/a/44583711/1151916) – Ramis Jun 16 '17 at 07:53
  • 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](https://stackoverflow.com/a/69079052/16602527). – Eric Sep 06 '21 at 18:48

10 Answers10

154

@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)
Phlippie Bosman
  • 5,378
  • 3
  • 26
  • 29
  • 11
    Any way to do this without the intermediate view controller "B" showing briefly during the transition? – shim Jun 08 '18 at 07:15
  • 2
    controller B still showing for a split second when transitioning to A. is there workaround to this problem? – axunic Jan 04 '19 at 20:21
  • 2
    @mnemonic23 I agree, the other VC is momentarily visible. Unfortunately I don't have a workaround. – Phlippie Bosman Jan 07 '19 at 10:55
  • See [my answer below](https://stackoverflow.com/a/69079052/16602527) for an easy solution *without* the animation glitch. – Eric Sep 06 '21 at 18:44
34

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: {})
Laszlo
  • 2,803
  • 2
  • 28
  • 33
Ken Toh
  • 3,721
  • 1
  • 24
  • 30
  • 8
    Did not work... it only dismisses the vc on the top of stack – codeinjuice Jun 22 '15 at 22:26
  • 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? – Ken Toh Jun 22 '15 at 22:32
  • The reason why it's not working is because self.presentingViewController is nilled in completion – Nils Ziehn Jun 22 '15 at 22:34
  • 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 Jun 22 '15 at 22:53
  • @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...) – codeinjuice Jun 29 '15 at 16:28
12

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)
Rafael
  • 7,002
  • 5
  • 43
  • 52
9

You should be able to call:

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

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

Nils Ziehn
  • 4,118
  • 6
  • 26
  • 40
  • 4
    How does this dismiss multiple view controllers? – Gruntcakes Jun 22 '15 at 22:05
  • 2
    It worked! I added: self.dismissViewControllerAnimated(true, completion: {}) self.presentingViewController?.dismissViewControllerAnimated(true, completion: {}) – codeinjuice Jun 22 '15 at 22:15
  • I srsly don't understand the downvotes. This will actually do what the OP asks. – Nils Ziehn Jun 22 '15 at 22:34
  • 1
    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 https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/dismissViewControllerAnimated:completion: – Ken Toh Jun 22 '15 at 23:21
  • @minsanity is right, I changed a little bit code here. – crazy_phage Oct 13 '15 at 02:22
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?

Community
  • 1
  • 1
Mixaz
  • 4,068
  • 1
  • 29
  • 55
5

I had some animation issues when trying the accepted answer in my application. The previously presented views would flash or try to animate on the screen. This was my solution:

     if let first = presentingViewController,
        let second = first.presentingViewController,
            let third = second.presentingViewController {
                second.view.isHidden = true
                first.view.isHidden = true
                    third.dismiss(animated: true)

     }
Aaron Halvorsen
  • 2,610
  • 1
  • 23
  • 31
  • 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 – A.Kant Feb 13 '19 at 15:57
  • 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 Sep 06 '21 at 18:16
  • 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](https://stackoverflow.com/a/69079052/16602527) that solves the animation glitch completely! – Eric Sep 06 '21 at 18:46
4

Adding on to Phlippie Bosman's answer, when calling

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

if you don't want to see (what would be the presentingViewController) you can do something like

self.presentingViewController?.view.addSubview(self.view)

This seems a bit hacky, but so far it's been the only way I've been able to make it seem like two view controllers are dismissing in unison.

0

Although Rafeels answer is acceptable. Not everybody uses Segue's.

For me the following solution works best

if let viewControllers = self.navigationController?.viewControllers {
   let viewControllerArray = viewControllers.filter { 
       $0 is CustomAViewController || $0 is CustomBViewController  }

    DispatchQueue.main.async {
      self.navigationController?.setViewControllers(viewControllerArray,
                                                    animated: true)
    }
}
Tom
  • 2,358
  • 1
  • 15
  • 30
0

The best way to achieve the OP's result WITHOUT the animation glitch (where the intermediate view controller briefly shows during dismissal) is to embed view controller A (the first vc) in a navigation controller, then simply place the line self.navigationController!.setViewControllers([self], animated: false) inside the ViewDidAppear method of view controller C (the top most vc).

For as the Apple Doc states:

[This] updates or replace the current view controller stack without pushing or popping each controller explicitly. In addition, this method lets you update the set of controllers without animating the changes

In other words, we're simply getting rid of the intermediate view controller (invisibly in the background) so that a simple self.dismiss(animated: true) will dismiss the only view controller that remains on the stack (i.e. view controller C).

Eric
  • 569
  • 4
  • 21
  • Does that mean the back button would go all the way back to the original parent view controller? What if you just want to go back to the previous screen? – omihek Dec 20 '22 at 23:34
  • I'm not sure I understand your question, because going back one screen is simply the default functionality of a navigation controller, you wouldn't have to do anything different... – Eric Dec 21 '22 at 14:51
  • My question is are you replacing the default functionality with this solution? So if I have a button on my screen that should take me back to the root controller, but then I also have the default back button, will they both do what they're supposed to do if your solution is implemented? – omihek Dec 21 '22 at 21:06
  • I'm not a 100% certain since I wrote those post so long ago, but I think that `setViewControllers` line would get rid of vc B, so you wouldn't be able to go back to it anymore. What you could do is move that line to AFTER you actually know what you wanna do. So if you press your special back button, quickly call that line, and then going back would skip vc B because it's no longer there. Something along those lines. – Eric Dec 22 '22 at 15:41
-1

Swift 4.0

 let presentingViewController = self.presentingViewController               
 presentingViewController?.presentingViewController?.presentingViewController?.dismiss(animated: false, completion: nil)
Pranit
  • 892
  • 12
  • 20