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)
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 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...
This is my StartViewController with simple #IBAction:
class StartViewController: UIViewController {
#IBAction func startButtonTapped(sender: UIButton) {
revealViewController().revealToggleAnimated(true) //error
}
}
Along with the commented line there is an error:
fatal error: unexpectedly found nil while unwrapping an Optional value
What am I doing wrong?
Of course StartViewController is presented from SWRevealViewController via sw_front segue.
For some reason revealViewController() returns nil here. Why?
This is how my storyboard looks like:
When you implementing SWRevealViewController using storyboard, you should set Storyboard Entry Point to SWRevealViewController instance (Reveal View Controller on picture), not to front view controller (Start View Controller on picture).
Note that revealViewController method return optional pointer, and even when all is set properly it return nil until view is loaded, so you better use optional chaining:
revealViewController()?.revealToggleAnimated(true)
I think your app might crash because you are missing a delegate.Try to add the following to your protocols:
class StartViewController: UIViewController,SWRevealViewControllerDelegate {
#IBAction func startButtonTapped(sender: UIButton) {
revealViewController().revealToggleAnimated(true) //error
}
}
If it still crashes, try changing the revealViewController().revealToggleAnimated(true) with revealViewController().revealToggle(self) and see if it still crashes.
Let's say I have a firstViewController and a secondViewController. The first one contains a firstButton and the second one - a secondButton. Here's what I want to do: when user clicks the secondButton, some firstButton's property changes.
Unfortunately, when I create an instance of a firstViewController in a secondViewController and then trying to access a firstButton, I get an error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
So, technically, I'm trying to do this as follows:
var ins = firstViewController()
#IBAction func secondButtonisPressed(){
ins.firstButton.alpha = 0
}
What is the proper way to implement that?
Thanks in advance.
Your problem here is that the IBOutlets of your firstViewController are only available (!= nil) after the viewDidLoad() firstViewController's method has being called.
In other words, you have to present the view, before you can make any changes to a UIViewController IBOutlet.
How you can solve this?
Add a variable into FirstViewController that works as a flag for you.
for example: var hideFirstButton = false
in the viewDidLoad or viewWillAppear method of FirstViewController check for hideFirstButton's value and hide or show your firstButton.
Then, before you present your FirstViewController change the value of hideFirstButton to the needed for your application to run fine.
UPDATE:
Other workaround, using Storyboard is (This approach has the inconvenient that the completion handler is called after viewWillAppear() so the button is visible for a second):
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let firstViewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as! FirstViewController
self.presentViewController(firstViewController, animated: true, completion: {
//This lines will be called after the view is loaded so the code will run
firstViewController.firstButton.alpha = 0
})
EXAMPLE: an example at GitHub
You could try to do this using delegation, similar to the way Apple does it in their existing frameworks. For an example, look at the way that you use UITableViewDelegate when working with a UITableView object.
If you wanted to use delegation to tell secondViewController that firstButton was pressed using delegation, you could do it as follows:
Step 1:
Create a protocol containing a method for the button press event.
protocol buttonPressDelegate {
func buttonPressed() -> Void
}
Step 2:
In firstViewController, declare that you have an instance of an object of type buttonPressProtocol.
var buttonPressDelegateObj: buttonPressDelegate?
Step 3:
In firstViewController, initialize your buttonPressDelegateObj to contain a reference to your instance of secondViewController. If you want you can create a method to set the reference contained in buttonPressDelegateObj, or do it viewDidLoad in firstViewController, etc.
buttonPressDelegateObj = secondViewControllerObj
Step 4:
In secondViewController, declare that you adopt the buttonPressDelegate protocol.
class secondViewController: UIViewController, buttonPressDelegate {
Step 5:
In secondViewController, implement the protocol method buttonPressed() by adding the function with your desired implementation. Here's an example:
func buttonPressed() {
secondButton.alpha = 0
}
Step 6:
Create an #IBAction on the button in firstViewController, so that when the button is pressed it calls buttonPressDelegateObj.buttonPressed() and you can respond to the event
#IBAction func firstButtonPressed() {
if (buttonPressDelegateObj != nil) {
buttonPressDelegateObj.buttonPressed()
}
else {
print("You forgot to set your reference in buttonPressDelegateObj to contain an instance of secondViewController!")
}
}
Note: This is just one way that you could do this. To tell firstViewController that secondButton was pressed (go the other way), have firstViewController implement the protocol buttonPressDelegate, have secondViewController contain a reference to firstViewController as an instance of type buttonPressDelegate?, and create an #IBAction in secondViewController that fires when secondButton is pressed that calls your the buttonPressDelegate method.
Note: There is a similar pattern employed in the Android world to get a Fragment to communicate to an Activity, that you can read more about here
I've recently been playing around with swift segues and I'd love to incorporate one in my latest app, the problem is I can't seem to get them to work. So far I've created another view controller SecondViewController and referenced in my ViewController & SecondViewController files as so:
ViewController.swift
import UIKit
class ViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
override func viewDidLoad() {
//lots more code here
SecondViewController.swift
import UIKit
class SecondViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
override func viewDidLoad() {
Them in storyboard view I've crtl+dragged a segue from viewController to secondViewController and once that's been created given that segue an identifier using the right hand panel, the segue identifier is GameOver and the segue type is show.
Now I want to call the segue automatically with no interaction from the user, in the final app once the user hits the game over func it would trigger the segue and display a new UIView where the highscore could be displayed with a few other items.
The code I'm using to call the segue is:
self.viewController.performSegueWithIdentifier("GameOver", sender: self)
I receive the following error...
Thread 1:EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0
I also have this error in the output field...
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
I've played around with the names of the segues and the file names and I still get the same error, I'm sure I'm missing something fundamental so hopefully someone can help me work this out.
I've created a new project and uploaded it to GitHub, if anyone could tell me what I'm missing that would be great, here is a link to my GitHub repository https://github.com/rich84ts/TestSingleView
Thank you.
You cannot just throw in some instance properties and expect them to magically do something:
class ViewController: UIViewController {
var secondViewController: SecondViewController!
var viewController: ViewController!
}
Those properties are nil, and sending a message to them will crash your app. You have to give them values.
In your case, the segue emanates from this view controller, so what you actually want to say is
self.performSegueWithIdentifier("GameOver", sender: self)
The other big mistake you are making is that you are saying all this in viewDidLoad. That is way too early! You can't do any segue-ing yet; your view is not even in the interface! Move your code into viewDidAppear: and it will actually work:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.performSegueWithIdentifier("GameOver", sender: self)
}
Your code is still silly and useless, but at least you will see something happen and you can continue developing from there.
What I actually recommend is that you delete your viewDidLoad implementation and put this:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
delay(1) {
self.performSegueWithIdentifier("GameOver", sender: self)
}
}
That will allow the first view controller to appear, wait one second, then summon the second view controller. And so you will learn that everything is hooked up correctly, and can proceed to do something more practical.
You can create a manual segue from the storyboard by control-clicking the ViewController object and dragging from the manual segue to the destination view controller. You can then call this segue with the designated identifier from your source controller. You don't need a reference to the destination view controller to achieve this.
To reference anything from the storyboard in your view controller you need to declare your properties like this:
#IBOutlet var someProperty : UIView?
The #IBOutlet bit makes the property visible on the storyboard and you can control-drag from it to a corresponding object in a view. You can't do this with view controllers though. To access the destination view controller in your source view controller before the segue you need to override func prepareForSegue(_ segue: UIStoryboardSegue,
sender sender: AnyObject?). This allows you to access the destination view controller from the segue-instance before the actual segue (if you need to pass it data for example).
Firstly your self.viewController is a nil object as you only created the variable and didn't initialize it. You can't call a method with nil object. Secondly you have created a push segue from storyboard but you don't have navigation controller in storyboard so self.performSegueWithIdentifier("GameOver", sender: self) will also not work. To use push segue you should have you current viewcontroller in UINavigationController's stack, so first add a UINavigationController in storyboard and make that initial view controller and set ViewController to the rootViewController of the navigation controller then call self.performSegueWithIdentifier("GameOver", sender: self)
Then the code will work. Hope this help.