How do I pass data between two scenes in Swift? - ios

I have two scenes where I would like to pass a single variable to another scene using a segue. I have tried but unfortunately all tutorials I have seen are dealing with the storyboard. I am not using the storyboard. I am doing all of this programatically.
Here is the segue i am trying to initialize:
func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "segueTest") {
var lose = segue.destinationViewController as! loserScene;
lose.toPass = scoreLabelNode.text
}
}
The Loser Scene is my second scene.
scoreLabelNode.text is the text of an NSTimer which i'm using as a score.
I want to move the scoreLabelNode.text into another scene which is my loserScene.
My loserScene is set up like this:
import SpriteKit
import Foundation
let GameOverLabelCategoryName = "gameOverLabel"
class loserScene: SKScene {
var toPass: String!
var scoreLabel = SKLabelNode(fontNamed:"[z] Arista Light")
override func didMoveToView(view: SKView) {
var gameOver = SKSpriteNode(imageNamed: "gameover.png")
gameOver.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
self.addChild(gameOver)
println(toPass)
}
}
I am trying to print 'toPass' to test the string segue, but I just get nil.

OK! So instead of using a segue. WHICH YOU CANNOT DO WITH AN SKSCENE. I used a struct. I put the struct outside of one of my scenes that I wanted data to be passed from, and made sure the data was being fed into the struct. I then accessed the struct from another scene and it works perfectly!
struct Variables {
static var aVariable = 0
}
this is the struct^ I have it set to 0 but it will be updated automatically when the score is recorded at the end of the run of my game.
I then accessed it like so in another scene:
print(Variables.aVariable)
This prints the variable from the struct in my new scene. I can also turn this struct into a string and use it for a labelnode.

Your toPass String returns nil because it is not initialized when you send the value from your prepareForSegue method.
Create a some function for example;
// Do whatever you need with the toPass variable.
func printToPassValue(){
print(toPass)
}
And add the property observers to your toPass variable. like this;
var toPass: String? {
willSet {
printToPassValue() // Call your function
}
}
And add this function to your viewDidLoad method.
override func viewDidLoad() {
super.viewDidLoad()
printToPassValue()
}

Related

Pass data from View Controller to Class?

First, I'm not pass data between view controllers. I need pass data from a View Controller to another class.
Situation likes this:
I have one View Controller, it has slider, then I have a Camera class. I defined a camera the Scene Class, once I change the slider, the value of the camera in the Scene class needs to be changed.
VC:
class ViewController: {
#IBAction func moveSlider(_ sender: NSSlider) {
// here I want to pass the sender.value to the Camera class
}
}
Camera class:
class Camera {
var cameraLocationX ; // I want this value updated once the the slider moved, however the camera instance is defined in the Scene
}
However, this Camera class are not defined in the VC. So I can't use the static var method...
class Scene {
let camera = Camera()
..
camera.cameraLocationX; // here it needs to be updated.
}
How to achieve this? I Googled it seemed that I should use delegate or notification , but can someone give me a little more instructions?
Use the delegation pattern. This is a core concept described https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
If you want to update property of class then you can set new value from your VC:
class ViewController: {
let camera = Camera()
#IBAction func moveSlider(_ sender: NSSlider) {
// here I want to pass the sender.value to the Camera class
camera.cameraLocationX = sender.value
}
}

Changing UILabel's text only works inside viewDidLoad()

I'm trying to make a function that I can call on to update my UILabel's scoreLabel.text. However, I get an error whenever I try to change it. What's confusing to me is that I don't receive an error when changing it inside viewDidLoad(). Everywhere else returns the following error:
In the console I also get error:
fatal error: unexpectedly found nil while unwrapping an Optional value.
So what I've been lead to believe is that when calling my function to update the text, the view hasn't loaded the UILabel yet. But I'm certain that this function is called only once the view has loaded.
Things I've checked/tried for:
That my IBOutlet is properly connected
That my function is being called
Using both scoreLabel.text and self.scoreLabel.text
Using a Strong and Weak outlet connection
I am also positive that changeTextLabel is being called after scoreLabel is loaded into memory. But again my error seems to say otherwise.
Here's a complete markup of my code. I've removed some irrelevant details for readability:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
#IBOutlet weak var scoreLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//This is where all my other code was
print(scoreLabel.text)
scoreLabel.text = "Testing" //This line can be run
}
func changeTextLabel() {
print("changeTextLabel called")
if self.scoreLabel != nil {
self.scoreLabel.text = "yay"
} else {
print("scoreLabel is nil") //This is called every time
}
}
}
Thanks for your time
Edit:GameScene.swift
This is only the part that should be of concern
func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
print("Hit")
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let object = storyBoard.instantiateViewController(withIdentifier: "GameViewController") as! GameViewController
object.changeTextLabel()
projectile.removeFromParent()
monster.removeFromParent()
}
I dont fully know your project setup but your approach seems complicated, its not what you should do in SpriteKit games. So I dont think its a good idea to tell you how to fix your current problem.
You should create your UI, such as labels, buttons etc using SpriteKit APIs (SKLabelNodes, SKSpriteNodes, SKNodes etc) directly in the relevant SKScene(s).
Your GameViewController should only really handle presenting SKScenes. so there should be next to no code there apart from loading the 1st SKScene.
If you have multiple SKScenes (MenuScene, ShopScene, SettingScene etc) your approach will fall apart because the score label will show in all SKScenes. GameViewController presents all your SKScenes, so whats added to GameViewController shows in all SKscenes. That means you have to remove/add labels and other UI for each scene and it will be chaos.
So to create a score label you should do this directly in the relevant scene. I like to use the lazy var approach to keep the setup code for the label in the same spot.
class GameScene: SKScene {
lazy var scoreLabel: SKLabelNode = {
let label = SKLabelNode(fontNamed: "HelveticaNeue")
label.text = "SomeText"
label.fontSize = 22
label.fontColor = .yellow
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return label
}()
override func didMove(to view: SKView) {
addChild(scoreLabel)
}
}
Than you can update it like this directly in the collision code you have.
scoreLabel.text = "NewText"
Now when you change from 1 SKScene to another SKScene you dont have to worry about removing this label, SpriteKit will do it for you.
You could also use xCodes level editor to add this label visually, similar to storyboards. This brings me to my final suggestion, which is that storyboards are not really used in SpriteKit games. Storyboards are for ViewControllers and in SpriteKit you are working with SKScenes.
Hope this helps
The problem is with this line,
let game = GameViewController()
game.changeTextLabel()
You should create GameViewController object using XIB or Storyboard.
In your case it is just creating object using .swift file, formally class.
You should create your object using View which is having IBOutlet connection with scoreLabel.
Try (If using Storyboard),
let storyboard = UIStoryboard(name: "YourStoryBoard", bundle: nil)
let gameVC = storyboard.instantiateViewController(withIdentifier: "GameViewController") as! UIViewController
or (If using XIB)
var gameVC = GameViewController(nibName: "GameViewController", bundle: nil)
Update you changeTextLabel with following (Ref),
func changeTextLabel() {
print(self.view)
print("changeTextLabel called")
if self.scoreLabel != nil {
self.scoreLabel.text = "yay"
} else {
print("scoreLabel is nil") //This is called every time
}
}
Replace this line
self.scoreLabel.text = "yay"
With
self.scoreLabel?.text = "yay"
Make these changes and try again:
func changeTextLabel() {
print("changeTextLabel called")
if self.scoreLabel != nil {
self.scoreLabel.text = "yay"
}
}
func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
print("Hit")
let game = STORYBOARD.instantiateViewControllerWithIdentifier("STORYBOARD_IDENTIFIER") as! GameViewController
game.changeTextLabel()
projectile.removeFromParent()
monster.removeFromParent()
}
It happens because you are not getting memory of label in any other function.It happens because of so many reasons.
One reason could be #IBOutlet weak var scoreLabel: UILabel!
Try to create a strong variable outlet.
#IBOutlet strong var scoreLabel: UILabel!
Second reason could be that you are calling changeTextLabel before the label gets memory. So call it later.

What's the cleanest way to call a method from one SKScene to another in Swift?

So I got in my game 3 different scenes: MenuScene, GameScene and GameOverScene.
I would like to know how I can get the score variable located in GameScene from GameOverScene so I can show it up there after the player loses.
So in GameScene, I created this simple getter:
func getScore() -> Int {
return self.score
}
But if I try to do GameScene.getScore() from a different Scene I get an error.
Tried using "class" before func
class func getScore() -> Int {
return self.score
}
But that also gives me an error from GameScene saying:
"Instance member 'score' cannot be used on type GameScene"
So how's the best way to do it? I don't want to create a global variable for this, it would be ugly.
This is quite easy actually. When you segue to the new scene, you can also pass in a variable with it... Here is an example.
GameOverScene.swift
var score:Int = 0
GameScene.swift
var score:Int = THE USERS SCORE
func segue(){
let gameScene = GameOverScene(size: self.size)
gameScene.score = score
let transition = SKTransition.doorsCloseHorizontalWithDuration(0.5)
gameScene.scaleMode = SKSceneScaleMode.AspectFill
self.scene!.view?.presentScene(gameScene, transition: transition)
}
You need to keep an instance of GameScene in your MenuScene class (recommended). Alternatively, you can store the score in a global storage medium.
To enter the game scene, you need to create it first, right?
let scene = SKScene(fileNamed: "blah blah blah")
view.presentScene(scene)
Now instead of creating the scene and assigning it to a local variable, you assign it to a class level variable.
var gameScene: GameScene?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (...) {
gameScene = SKScene(fileNamed: "blah blah blah")
view.presentScene(gameScene!)
}
}
Then, when appropriate, you can access the score like this:
gameScene.getScore()
Another way is to use NSUserDefaults, but you don't seem to like it.
The final way that I can think of is to use static variables. You put all the stuff that you need to pass between the two scenes in a class as static variables. The disadvantage of this is that unless you declare the class private and put both scenes and the class in the same file, others classes can access and change the variables. This reduces maintainability.
Explicit getters are not needed in Swift, btw.
There are many ways to achieve what do you want to do. You could use a manager class , a shared instance to maintain your game variables.
Why this choice?
Because you will want to save your values or your profile or you will want to reset all the game progress, because you will want to select the old 2nd level when you have unlocked the 18th level, or repeat a failed level without accumulating values. It's fast to do it and you can handle separately your common game variables (it seems that I'm selling pots in an advertisement..)
class Settings: NSObject { // NSCoding in case you want to save your data
var score: Int = 0
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeInteger(score, forKey: "score")
}
init(coder aDecoder: NSCoder!) {
score = aDecoder. decodeIntegerForKey("score")
}
override init() {
super.init()
// do whatever you want to initializing your settings
}
}
class GameManager: NSObject {
static let sharedInstance = GameManager()
var settings : Settings! = Settings()
}
class MenuScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}
class GameScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}
class GameOverScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}

Error trying to transfer data from variable between storyboards

I have a game where I store the value of the high score of a player as "highScore", in the first view controllers.
What I want to do is to try to transfer this variable data, to the second view controller.
I entered in the following code in the first view controller's storyboard.
Btw, highscore2, is the variable that I delcared in the second view controller, to store the data from the first highscore variable.:
override func prepareForSegue(segue: UIStoryboardSegue!, sender:AnyObject!)
{
if (segue.identifier == "segueTest")
{
var svc = segue!.destinationViewController as! viewTwo;
svc.highScore2 = "High Score \(highScore)"
}
}
Now here is the code in my second view controller (viewTwo):
class viewTwo: UIViewController
{
#IBOutlet var highScoretwolbl: UILabel!
var highScore2:String!
override func viewDidLoad() {
highScoretwolbl.text = highScore2
}
}
For some reason, the code compiles, but the high score is not displayed, and is "nil".
Avoid forced unwrapping.
if let svc = segue.destinationViewController as? viewTwo{
svc.highScore2 = "High Score \(highScore)"
}
Then put in a breakpoint and make sure you got to all of these lines.
As stated in other answers, viewDidLoad only gets fired once. Since you
are using a forced unwrapped string, it is initially nil. It (highScore2) won't get a value until you set it which happens after viewDidLoad. Add some print statements so you can see the sequence of events.
Your best option is to use viewWillAppear. This will get fired every time
your view appears. This is what you want.
You don't need to use a force unwrapped variable. Just make a string a
make it empty initially.
override func prepareForSegue(segue: UIStoryboardSegue, sender:AnyObject?)
{
if segue.identifier == "segueTest"
{
if let svc = segue.destinationViewController as? viewTwo {
print("setting score in prepareForSegue")
svc.highScore2 = "High Score \(highScore)"
}
else {
print("something bad happened")
}
}
}
class viewTwo: UIViewController
{
#IBOutlet var highScoretwolbl: UILabel!
// The following uses forced upwrapping which means it could be nil
// if you never set it and your app will crash.
// var highScore2: String!
// Instead, do this:
var highScore2: String = ""
override func viewWillAppear(animated: Bool) {
// putting this here will ensure that every time the screen
// is shown, the label will get set from the string.
highScoretwolbl.text = highScore2
}
}
If I recall correctly, the viewDidLoad method is called only once when the view controller is first initialized. So you are setting the highScoretwolbl variable before highScore2 actually has any data, this is why it is nil.
I would recommend setting the text inside viewWillAppear so it updates every time the segue happens.
So something like
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
highScoretwolbl.text = highScore2
}
Here is a reference to the lifecycle of all the methods of a view controller. View controller lifecycle

Passing data between views in ONE ViewController in Swift

All of the searches I've done focus on passing data between view controllers. That's not really what I'm trying to do. I have a ViewController that has multiple Views in it. The ViewController has a slider which works fine:
var throttleSetting = Float()
#IBAction func changeThrottleSetting(sender: UISlider)
{
throttleSetting = sender.value
}
Then, in one of the Views contained in that same ViewController, I have a basic line that (for now) sets an initial value which is used later in the DrawRect portion of the code:
var RPMPointerAngle: CGFloat {
var angle: CGFloat = 2.0
return angle
}
What I want to do is have the slider's value from the ViewController be passed to the View contained in the ViewController to allow the drawRect to be dynamic.
Thanks for your help!
EDIT: Sorry, when I created this answer I was having ViewControllers in mind. A much easier way would be to create a method in SomeView and talk directly to it.
Example:
class MainViewController: UIViewController {
var view1: SomeView!
var view2: SomeView!
override func viewDidLoad() {
super.viewDidLoad()
// Create the views here
view1 = SomeView()
view2 = SomeView()
view.addSubview(view1)
view.addSubview(view2)
}
#IBAction func someAction(sender: UIButton) {
view1.changeString("blabla")
}
}
class SomeView: UIView {
var someString: String?
func changeString(someText: String) {
someString = someText
}
}
Delegate:
First you create a protocol:
protocol NameOfDelegate: class { // ": class" isn't mandatory, but it is when you want to set the delegate property to weak
func someFunction() // this function has to be implemented in your MainViewController so it can access the properties and other methods in there
}
In your Views you have to add:
class SomeView: UIView, NameOfDelegate {
// your code
func someFunction() {
// change your slider settings
}
}
And the last step, you'll have to add a property of the delegate, so you can "talk" to it. Personally I imagine this property to be a gate of some sort, between the two classes so they can talk to each other.
class MainViewController: UIViewController {
weak var delegate: NameOfDelegate?
#IBAction func button(sender: UIButton) {
if delegate != nil {
let someString = delegate.someFunction()
}
}
}
I used a button here just to show how you could use the delegate. Just replace it with your slider to change the properties of your Views
EDIT: One thing I forgot to mention is, you'll somehow need to assign SomeView as the delegate. But like I said, I don't know how you're creating the views etc so I can't help you with that.
In the MVC model views can't communicate directly with each other.
There is always a view controller who manages the views. The views are just like the controllers minions.
All communication goes via a view controller.
If you want to react to some view changing, you can setup an IBAction. In the method you can then change your other view to which you might have an IBOutlet.
So in your example you might have an IBAction for the slider changing it's value (as in your original question) from which you could set some public properties on the view you would like to change. If necessary you could also call setNeedsDisplay() on the target view to make it redraw itself.

Resources