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.
Related
base ViewController
import UIKit
class SubViewPost: UIViewController {
#IBOutlet weak var content: UILabel!
#IBOutlet weak var recommendCount: UILabel!
#IBOutlet weak var recommendButton: UIButton!
var postInfo:PostInfo!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
child ViewController
import UIKit
class SubViewOne: SubViewPost {
#IBAction func likeWorry(_ sender: Any) {
Option.recommend(postInfo: postInfo, mRecommendCount: recommendCount, mRecommendButton: recommendButton)
}
}
and another child viewController
import UIKit
class SubViewTwo: SubViewPost {
override func viewDidLoad() {
recommendCount.alpha=0
recommendButton.alpha=0
}
}
i want add subviewOne or SubViewTwo
My ParentView
var subViewPost:SubViewPost
if postType == 1{
subViewPost = storyboard?.instantiateViewController(withIdentifier: "SubViewPost") as! SubViewOne
}else{
subViewPost = storyboard?.instantiateViewController(withIdentifier: "SubViewPost") as! SubViewTwo
}
containerView.addSubview(subViewPost.view)
raise error
Could not cast value of type
'MyApp.SubViewPost' (0x101151728) to 'MyApp.SubViewOne' (0x10114d9d0).
2018-07-10 14:40:56.007436+0900 MyApp[7207:209932]
Could not cast value of type 'MyApp.SubViewPost' (0x101151728) to 'MyApp.SubViewOne' (0x10114d9d0).
how to chagne view controller by According to postType
SubView One have Recommned
but SubView Two haven't Recommend
SubView 1,2 have same UI
The UViewController class for your scene "SubViewPost" in your storyboard is set to SubViewPost and that is what instantiateViewController will be returning. You cannot downcast an instance of SubViewPost to SubViewOne or SubViewTwo.
You could define two identical scenes in your storyboard, each with the appropriate view controller class, but that would require a lot of duplication.
Since the only difference is the visibility of the recommendButton and recommendCount elements, why not just handle that via a property:
var subViewPost = storyboard?.instantiateViewController(withIdentifier: "SubViewPost") as! SubViewPost
subViewPost.recommendVisible = (postType == 1)
SubViewPost.swift
var recommendVisible = true
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
recommendCount.isHidden = !recommendVisible
recommendButton.isHidden = !recommendVisible
}
The error message is clear. When you say
storyboard?.instantiateViewController(withIdentifier: "SubViewPost")
what you get from the storyboard is a view controller whose class is SubViewPost. You cannot wave a magic casting wand and claim that it is a SubViewOne instead; it isn't.
If you wanted this view controller to be a SubViewOne, then you should have declared it as a SubViewOne in the storyboard in the Identity inspector.
I think I see what you are trying to do, and why you are confused about why you can't do it this way.
What's in the storyboard is an instance, not a class. Yes, it is an instance of some class, but it is an instance of that class. So when you design the interface in the storyboard, you are designing the interface associated with that one instance of that one class.
If your goal is to have a single interface associated with multiple classes, the interface must be generated in code or loaded from a View .xib file — not designed in a storyboard.
However, you would be better off not trying to use subclassing in this situation in the first place. What I do in a similar situation is give my view controller an enum property that says which "kind" of view controller it is, and obey accordingly in code. That a way, a single class serves multiple purposes.
I have the following problem, I am building a school schedule and I have 2 viewControllers with scrollView in each. In the viewController "Bloques" I have a containerView at the side of the list (The list is inside a scrollView), which will contain the "Dias" viewController, the idea is that when I switch from one day to another using paging in "Dias", don't move the list of bloques. The problem is that the list is too long, so I use the scrollView in both viewController, which have the same number of frames, then when I scroll on the Y axis for example "Bloques", I want it to also perform Scroll in the "Dias" scrollView. In another project I did something similar, but within the same viewController, which did not cause me major complications, but being in different viewControllers generates the following error:
Fatal error: unexpectedly found nil while unwrapping an Optional value:
Error:
Structure of viewControllers:
BLOQUES
#IBOutlet weak var bloquesScrollView: UIScrollView!
override public func viewDidLoad() {
super.viewDidLoad()
bloquesScrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.restorationIdentifier == "bloques" {
let ejey = bloquesScrollView.contentOffset.y
let pruebaController: prueba = prueba()
pruebaController.mover(y: Int(ejey))
}
}
DIAS
#IBOutlet weak var diasScrollView: UIScrollView!
func mover(y: Int?) {
print("casi \(y)")
diasScrollView.setContentOffset(CGPoint(x: 0, y: y!), animated: false)
}
override func viewDidLoad() {
super.viewDidLoad()
diasScrollView.delegate = self
}
Correcting the question, I look for a method in which different UIViewController (Monday, Tuestaday, etc) which contain ScrollView corresponding to blocks in the UIViewController "Bloques", give the movement signal to the ScrollView of "Blocks" if they too move or vice versa.
It should be mentioned that Monday, Tuesday, etc. Are contained in a Container View in "Bloques".
I understand that Container View controller manages a view just like any other UIViewController subclass, in this case, the image in the middle is the UIViewController of the Container View, which will run Monday, Tuesday, etc. Previously what I realized was a list of "Bloques" was inside each UIViewController (Monday, Tuesday, etc.), so it was easier to do it, but the view is strange, since the listing should be static and not be move and appear again on the next page (From Monday to Tuesday for example). I do not know if there is an easier method than the one I am proposing.
New Situation
EDIT
The problem is that in your first code let pruebaController: prueba = prueba() creates a whole new prueba instance. That is not the same prueba that has a scrollView, so its scrollView is nil and you crash when you try to use it.
Please try this code..
BLOQUES
#IBOutlet weak var bloquesScrollView: UIScrollView!
override public func viewDidLoad() {
super.viewDidLoad()
bloquesScrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.restorationIdentifier == "bloques" {
let ejey = bloquesScrollView.contentOffset.y
// try this code....
let vc = UIStoryboard(name: '<Here Your storyboard name (default : "main">', bundle: nil)
.instantiateViewController(withIdentifier: '<Here Your DIAS Identifier>') as! prueba
vc.mover(y: Int(ejey))
}
}
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
Full code for this branch here
View controller "MovieDetailsVC" is presented to the navigation controller when a cell is selected.
The presenting view controller, "ViewController", stores the row of the tableView to display in NSUserDefaults as an Int.
"MovieDetailsVC" reads the row ok. It then pulls the whole array of custom class info from CoreData and stores the array row in a property.
The data is displayed ok at first. The IBOutlet connections are setup ok. I've disconnected and reconnected twice all outlets on MovieDetailsVC, so that should be ok.
"viewDidLoad" is called a successive time. Not sure from where. When it is called, the coredata entity and row number are pulled ok.
The issue is at line "lblTitle.text = self.movieRecord.title". I'm assuming any of the IBOutlets would cause the same issue.
The error thrown is what you would see if the outlets were not connected:
fatal error: unexpectedly fond nil while unwrapping Optional value.
code for the MovieDetailsVC is below. Any ideas why this outlet link would break after working ok would be greatly appreciated.
import UIKit
import CoreData
class MovieDetailsVC: UIViewController {
#IBOutlet var lblTitle: UILabel!
#IBOutlet var lblDescr: UILabel!
#IBOutlet var lblLink: UILabel!
#IBOutlet var imgMovie: UIImageView!
var movieRecord:FavMovie!
var favMovies = [FavMovie]()
override func viewDidLoad() {
super.viewDidLoad()
fetchAndSetResult()
}
func fetchAndSetResult() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "FavMovie")
do {
let results = try context.executeFetchRequest(fetchRequest)
self.favMovies = results as! [FavMovie]
} catch let err as NSError {
print(err.description)
}
if let row = NSUserDefaults.standardUserDefaults().objectForKey("movieRow") as? Int {
self.movieRecord = self.favMovies[row]
configureCellDescr()
}
}
func configureCellDescr() {
lblTitle.text = self.movieRecord.title
lblDescr.text = self.movieRecord.descrWhyGood
lblLink.text = self.movieRecord.linkImdb
imgMovie.image = self.movieRecord.getImg()
}
}
I just have a look at your source code in github and find the problem. There are two issues and I will explain it following.
it does that the second time that the app overrides viewdidload
The reason that your code would call the viewDidLoad method twice is because you have a segue in your storyboard that connect the tableViewCell to movieDetailVC. This will present the movieDetailVC once you click the cell.
And in your code for didSelectCell method, you create another movieDetailVC object and present it.
So actually movieDetailVC would be presented twice when you click the cell. This cause the issue.
Any ideas why this outlet link would break after working ok would be greatly appreciated
The reason why the IBOutlet is nil is because of the way you present movieDetailVC in your code. You create the movieDetailVC object using: let movieDetailsVC = MovieDetailsVC(). Doing it this way, the IBOutlets will not be connected correctly, because ios dont know about the storyboard information.
The correct way to create a MovieDetailVC object is to instantiate from storyboard. See this post for difference.
Solution
There is a very simple solution for your code design:
just remove let movieDetailsVC = MovieDetailsVC() and self.navigationController?.presentViewController(movieDetailsVC, animated: true, completion: nil) from your ViewController class. Since you save the selection data in NSUserDefault, the cell segue will present movieDetailVC and your movieDetailVC can also get the selection data from userDefault.
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()
}