I'm completely stuck at trying to perform a segue out of the 5th and final scene of my SpriteKit game to another View Controller in the project(not the GameViewController, nor the root view controller).
I tried running self.view!.window!.rootViewController!.performSegueWithIdentifier("finalSegue", sender: self), from my finalScene, but it literally does nothing (the line gets triggered, it reads the right ViewController - i check by "print(self.view!.window!.rootViewController!)" console prints "segue read" as I instructed it, right after the segue command, as a check, the segue identifier is correct, but nothing happens).
Have tried calling a method that performs the segue from the GameViewController ( the view controller from which I am launching the view of the 5 SKScenes), I get "unexpectedly found nil while unwrapping an optional value". Tried performing the segue from the final scene ("finalScene.swift"), same error.
Have tried everything and all relevant solutions in other questions in the forum, as well as all combinations of nil/self/viewController in the "sender:" field of the performSegue method, to no avail. Here is the code that I am trying to make work which gives "unexpectedly found nil while unwrapping an optional value", pointing at the viewController var, but giving uncomprehensible debugging when loaded both on the device and on the simulator. It seems "nil" passes into the viewController var I am declaring, instead of the original GameViewController?
All my segue identifiers are correct in Storyboard, everything checked multiple times...What am I doing wrong? Should I do something different, given its the 5th SKScene and not the 1st (as in other solutions)? The segue into the SKScenes by segueing into the GameViewController from another UIViewController works fine - its the exit out of them that does not work. Many thanks for any help, completely stuck here!
Here is my relevant code:
In my GameViewController (UIViewController that launches my 5 consecutive SKScenes):
class GameViewController: UIViewController {
let myScene = finalScene()
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
//this is the 1st out of 5 SKScenes in the project
let scene = GameScene(size: CGSize(width: 2048, height: 2742))
//this is the 5th out of 5 scenes, that I am trying to trigger the segue out of
myScene.viewController = self
view.presentScene(scene)
view.ignoresSiblingOrder = true
}
}
}
Im my 5th scene, finalScene:
class finalScene: SKScene {
var viewController: GameViewController!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let positionOfTouch = touch.location(in: self)
let tappedNode = atPoint(positionOfTouch)
let nameOfTappedNode = tappedNode.name
if nameOfTappedNode == "continue" {
self.viewController.performSegue(withIdentifier: "finalSegue", sender: self)
}
}
}
Just for anyone who might come across this, I solved it by using notification center. Ie added a notification observer on the parent view controller of the game scenes (which calls a function that performs the segue), and at the end point in the game where I wanted to do the segue, I posted to that notification, and it works great.
adding observer in ViewDidLoad of UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.doaSegue), name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
}
func doaSegue(){
performSegue(withIdentifier: "toNext", sender: self)
self.view.removeFromSuperview()
self.view = nil
}
And then calling it from within the game SKScene:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
You are calling the segue on the root viewController. I think that is the problem. You need to call the segue on the scene's viewController instead (where I am assuming you have created the segue, hence it is not being found on the root viewController).
Now the problem is that an SKScene does not have direct access to it's viewController, but just the view in which it is contained. You need to create a pointer to it manually. This can be done by creating a property for the SKScene:
class GameScene: SKScene {
var viewController: UIViewController?
...
}
Then, in the viewController class, just before skView.presentScene(scene)
scene.viewController = self
Now, you can access the viewController directly. Simply call the segue on this viewController:
func returnToMainMenu(){
self.viewController.performSegueWithIdentifier("menu", sender: vc)
}
Found this method to work just as well. But I do prefer the method mentioned above since it's fewer lines. I do wonder which is better for performance...
Related
I know this question has been asked countless times already, and I've seen many variations including
func performSegue(withIdentifier identifier: String,
sender: Any?)
and all these other variations mentioned here: How to call a View Controller programmatically
but how would you change a view controller outside of a ViewController class? For example, a user is currently on ViewController_A, when a bluetooth device has been disconnected (out of range, weak signal, etc) the didDisconnectPeripheral method of CBCentral gets triggered. In that same method, I want to change current view to ViewController_B, however this method doesn't occur in a ViewController class, so methods like performSegue won't work.
One suggestion I've implemented in my AppDelegate that seems to work (used to grab the appropriate storyboard file for the iphone screen size / I hate AutoLayout with so much passion)
var storyboard: UIStoryboard = self.grabStoryboard()
display storyboard
self.window!.rootViewController = storyboard.instantiateInitialViewController()
self.window!.makeKeyAndVisible()
And then I tried to do the same in my non-ViewController class
var window: UIWindow?
var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) //assume this is the same storyboard pulled in `AppDelegate`
self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "ViewController_B")
self.window!.makeKeyAndVisible()
However I get an exception thrown saying fatal error: unexpectedly found nil while unwrapping an Optional value presumably from the window!
Any suggestions on what I can do, and what the correct design pattern is?
Try this:
protocol BTDeviceDelegate {
func deviceDidDisconnect()
func deviceDidConnect()
}
class YourClassWhichIsNotAViewController {
weak var deviceDelegate: BTDeviceDelegate?
func yourMethod() {
deviceDelegate?.deviceDidDisconnect()
}
}
class ViewController_A {
var deviceManager: YourClassWhichIsNotAViewController?
override func viewDidLoad() {
deviceManager = YourClassWhichIsNotAViewController()
deviceManager.delegate = self
}
}
extension ViewController_A: BTDeviceDelegate {
func deviceDidDisconnect() {
DispatchQueue.main.async {
// change the VC however you want here :)
// updated answer with 2 examples.
// The DispatchQueue.main.async is used here because you always want to do UI related stuff on the main queue
// and I am fairly certain that yourMethod is going to get called from a background queue because it is handling
// the status of your BT device which is usually done in the background...
// There are numerous ways to change your current VC so the decision is up to your liking / use-case.
// 1. If you are using a storyboard - create a segue from VC_A to VC_B with an identifier and use it in your code like this
performSegue(withIdentifier: "YourSegueIdentifierWhichYouveSpecifiedInYourSeguesAttibutesInspector", sender: nil)
// 2. Instantiate your VC_B from a XIB file which you've created in your project. You could think of a XIB file as a
// mini-storyboard made for one controller only. The nibName argument is the file's name.
let viewControllerB = ViewControllerB(nibName: "VC_B", bundle: nil)
// This presents the VC_B modally
present(viewControllerB, animated: true, completion: nil)
}
}
func deviceDidConnect() {}
}
YourClassWhichIsNotAViewController is the class which handles the bluetooth device status. Initiate it inside the VC_A and respond to the delegate methods appropriately. This should be the design pattern you are looking for.
I prefer dvdblk's solution, but I wasn't sure how to implement DispatchQueue.main.async (I'm still pretty new at Swift). So this is my roundabout, inefficient solution:
In my didDisconnectPeripheral I have a singleton with a boolean attribute that would signify whenever there would be a disconnect.
In my viewdidload of my ViewController I would run a scheduledTimer function that would periodically check the state of the boolean attribute. Subsequently, in my viewWillDisappear I invalidated the timer.
I'm trying to make my App to automatically go back to the main menu after 2 minutes of inactivity. So far I have both parts working, but not together..
The App starts a counter if there's no touch input:
see user4806509's anwer on Detecting when an app is active or inactive through touches in Swift
And from my main viewcontroller I can control the segue I need with code:
func goToMenu()
{
performSegueWithIdentifier("backToMenu", sender: self)
}
I've implemented the code from Rob's answer on How to call performSegueWithIdentifier from xib?
So I've created the following class and protocol:
protocol CustomViewDelegate: class {
func goToMenu()
}
class CustomView: UIView
{
weak var delegate: CustomViewDelegate?
func go() {
delegate?.goToMenu()
}
}
The Function go() gets (successfully) called when the timer runs out. But the delegate?.goToMenu() doesn't work. If I change it to: delegate!.goToMenu(), the App crashes with:
fatal error: unexpectedly found nil while unwrapping an Optional value
My main viewcontroller is a CustomViewDelegate and the viewDidLoad contains:
let myCustomView = NSBundle.mainBundle().loadNibNamed("customView", owner: self, options: nil)[0] as! CustomView
myCustomView.delegate = self
I have the correct xib file, and that part is working.
I can't seem to find the solution to this seemingly easy problem, does anyone have a fix? Or better yet, a more elegant solution to my problem?
Thank you!
edit: SOLUTION:
I've removed all my old code and implemented the NSNotification method:
In the UIApplication:
let CallForUnwindSegue = "nl.timfi.unwind"
func delayedAction()
{
NSNotificationCenter.defaultCenter().postNotificationName(CallForUnwindSegue, object: nil)
}
In the main ViewController:
let CallForUnwindSegue = "nl.timfi.unwind"
func goToMenu(notification: NSNotification)
{
performSegueWithIdentifier("backToMenu", sender: self)
}
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
In the viewDidLoad: NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PeriodViewController.goToMenu), name:CallForUnwindSegue , object: nil)
I believe that your issue is that because your delegate is declared as a weak var, your delegate is getting disposed while you wait for the timer to complete, thus the reference to the optional is lost (and returns nil when you force unwrap it).
try removing the weak keyword from your CustomView class.
Another solution would be to use the notification center to notify your view to call for the segue.
Something along the lines of what I proposed here
I hope this helps.
I have tried several different answers and have yet to find an answer that works.
I keep getting
fatal error: unexpectedly found nil while unwrapping an Optional value
I have a SKScene called selectUpgrade that is in my GameViewController and I am trying to segue to a UIViewController called MissileUpgrade.
var vc2 = MissileUpgrade() //trying to get to this UIViewController
var gameVC = GameViewController() // currently in a scene in this UIViewController
I am calling this in my scene to segue
func goToMissileUpgrade() {
gameVC.presentViewController(vc2, animated: true, completion: nil)
}
These are in the same storyboard. If I set MissileUpgrade as the initial VC it will load fine so I know it has nothing to do on that end. I am lost on why this is not working. Thanks for your help!
If you are using storyboards, don't use presentViewController use performSegueWithIdentifier
Give your Segue an Identifier in the Storyboard, then refer to it in the code like so:
self.performSegueWithIdentifier("yourSegueIdentifierHere", sender: self)
I'm currently trying to implement IAP into my SKScene like this Swift Sprite Kit In App Purchase
but I'm having a problem figuring out how to set self.canDisplayBannerAds to false from my skscene, The code I used did nothing. Any help would be appreciated.
func removeAds() {
let viewController: GameViewController = GameViewController()
viewController.canDisplayBannerAds = false
}
The reason why your code isn't working is because you are creating a NEW GameViewController and setting canDisplayBannerAds on that.
You need to keep a reference to your original GameViewController which you should be able to access from within your scene via your scenes SKView.
If you don't want to subclass your SKView you can use the following to get your current viewController.
if let viewController = view.nextResponder as? GameViewController /* or whatever your VC is */ {
viewController.canDisplayBannerAds = false
}
If your SKView is a subview change view.nextResponder to view.superview.nextResponder.
Alternatively you can use Notification Center to send a message to your GameViewController
In your GameViewController.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "turnOffAds", name: "TurnOffAdsNotification", object: nil)
}
func turnOffAds() {
self.canDisplayBannerAds = false
}
Somewhere in your SKScene:
NSNotificationCenter.defaultCenter().postNotificationName("TurnOffAdsNotification", object: nil)
In my GameScene.swift file, I am trying to perform a segue back to my Menu View Controller like so:
func returnToMainMenu(){
var vc: UIViewController = UIViewController()
vc = self.view!.window!.rootViewController!
vc.performSegueWithIdentifier("menu", sender: vc)
}
This method is run when a node is tapped:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if gameOn == false{
if restartBack.containsPoint(location){
self.restartGame()
}
else if menuBack.containsPoint(location){
self.returnToMainMenu()
}
else if justBegin == true{
self.restartGame()
}
}
}
}
Where menuBack is the button back to the menu. Every time I run this code, I am thrown an NSException:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Receiver (<ProxyBlock.Menu: 0x165a3e90>) has no segue with identifier 'menu''
I checked my segue's identifier and it was indeed "menu".
You are calling the segue on the root viewController. I think that is the problem. You need to call the segue on the scene's viewController instead (where I am assuming you have created the segue, hence it is not being found on the root viewController).
Now the problem is that an SKScene does not have direct access to it's viewController, but just the view in which it is contained. You need to create a pointer to it manually. This can be done by creating a property for the SKScene:
class GameScene: SKScene {
weak var viewController: UIViewController?
...
}
Then, in the viewController class, just before skView.presentScene(scene)
scene.viewController = self
Now, you can access the viewController directly. Simply call the segue on this viewController:
func returnToMainMenu(){
viewController?.performSegueWithIdentifier("menu", sender: vc)
}
How to segue from Scene to ViewController
Swift 3 - Works with SpriteKit / UIKit
You can use NSNotification.
Example:
1.) Create a segue in the storyboard and name the identifier "segue"
2.) Create a function in the ViewController you are segueing from.
func goToDifferentView() {
self.performSegue(withIdentifier: "segue", sender: self)
}
3.) In the ViewDidLoad() of your ViewController you are segueing from create the observer.
NotificationCenter.default.addObserver(self, selector: #selector(goToDifferentView), name: "segue" as NSNotification.Name, object: nil)
4.) In the ViewController or Scene you are segueing to, add the Post Method wherever you want the segue to be triggered.
NotificationCenter.default.post(name: "segue" as NSNotification.Name, object: nil)