Reputation: 694
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()
}
}
Upvotes: 53
Views: 47605
Reputation: 12194
Most elegant method of doing this in Swift 5 using Eric's answer (without the animation glitch) with a UINavigationController
:
extension UINavigationController {
/// This method manipulates the viewController's array to get rid of any intermediate view controllers so that the pop animation is clean and uninterrupted.
/// For instance, if you wish to pop off 2 viewControllers at the same time, you would either pop one off with `animation: false` and then the last one with
/// `animation: true`, but that leaves a problem in that the first viewController's, animation displays instead of the last one. And in the inverse case the
/// last viewController's animation appears but ends up at the second to last viewController before suddenly disappearing.
// Examples:
// 1) remove second to last item (this occurs when numVCs = 2):
// -3 -2 -1
// [..., vc1, vc2, vc3]
// ^
// this view controller is removed
// 2) remove second and third to last items (this occurs when numVCs = 3):
// -4 -3 -2 -1
// [..., vc1, vc2, vc3, vc4]
// ^ ^
// these view controllers are removed
func popViewControllers(_ numVCs: Int) {
guard numVCs > 1 else {
popViewController(animated: true)
return
} // use the Apple provided popViewController() for a single view controller
guard let toRemoveRange = Range(NSRange(location: viewControllers.count - numVCs, length: numVCs - 1)) else { return }
viewControllers.removeSubrange(toRemoveRange)
setViewControllers(viewControllers, animated: false)
popViewController(animated: true)
}
}
Usage:
navigationController.popViewControllers(4)
This tends to be more elegant than using Apple's provided popToViewController()
in cases where the navigation stack's ordering is known as holding and providing a reference to popToViewController()
is not required, just the number of VCs to pop off the stack.
Upvotes: 0
Reputation: 763
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).
Upvotes: 1
Reputation: 2418
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)
}
}
Upvotes: 0
Reputation: 7242
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:
You will be given a list of segues to choose from including the one we just created:
You should now have a segue, click on it:
Go in the inspector and set a unique id:
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)
Upvotes: 13
Reputation: 61
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.
Upvotes: 6
Reputation: 930
Swift 4.0
let presentingViewController = self.presentingViewController
presentingViewController?.presentingViewController?.presentingViewController?.dismiss(animated: false, completion: nil)
Upvotes: -1
Reputation: 2680
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)
}
Upvotes: 5
Reputation: 3751
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: {})
Upvotes: 34
Reputation: 5710
@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)
Upvotes: 155
Reputation: 4178
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?
Upvotes: 5
Reputation: 4331
You should be able to call:
self.presentingViewController.dismissViewControllerAnimated(true, completion: {});
(You may need to add ?
or !
somewhere - I'm not a swift developer)
Upvotes: 9