Why isn't view available? - swift

Why do I get:
fatal error: unexpectedly found nil while unwrapping an Optional value
while switching to the next scene and back?
// Main Menu Scene
class MainMenuScene: SKScene {
override func didMoveToView(view: SKView) {
// self.backgroundColor = UIColor.whiteColor()
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "switchScene", userInfo: nil, repeats: true)
println("loaded MainMenuScene")
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
func switchScene() {
println("switching to GameScene")
var game = GameScene(size: CGSizeMake(2048, 1536))
game.scaleMode = .AspectFill
if self.view == nil {
println("no self.view")
} else {
self.view!.presentScene(game)
}
}
}
// Game Scene
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
// self.backgroundColor = UIColor.whiteColor()
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "switchScene", userInfo: nil, repeats: true)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
func switchScene() {
// make var a member variable or this won't work
timer.invalidate()
var menu = MainMenuScene(size: CGSizeMake(2048, 1536))
menu.scaleMode = .AspectFill
self.view!.presentScene(menu)
}
}
First, change timer variable to a member.
Second, invalidate the timer.
Everything runs as expected.

From the SKScene docs:
To present a scene, you call the presentScene: method or presentScene:transition: method on the SKView class. If the scene is not currently presented, this property holds nil.
view is nil because the your GameScene (self) is not currently presented when this code runs. Your app crashes because you are force-unwrapping something that's nil.

Actually, Aaron was right. If you put remove the timer, or don't add enough time, then the view will be nil. In this instance, with the timer, by time the timer trigged, view did have a view initially.
What I was forgetting to do was invalidate() my timer before presenting the new SKScene. This kept the timer running, and when it triggers, view! is really nil because the object was removed.

Related

Custom MapView Class Swift

I'm having some major issues and could use some help. I'm using some code from the company What3Words. They created a custom class:
open class MapView: MKMapView, MKMapViewDelegate, UIGestureRecognizerDelegate, CLLocationManagerDelegate {
//all their code is here
}
My goal is to use a tap gesture in order to acquire a lat/long from the map. I can do this incredibly easily when using a MapViewKit drop in and I make the object conform to MKMapView!
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapped(_ sender: UITapGestureRecognizer) {
print("hey")
let tap = sender
let coord = tap.location(in: map)
print(coord)
print(tap.location(in: self))
print(tap.location(in: map))
map.convert(coord, to: map)
print(map.convert(coord, to: map))
}
}
But, if I conform it MKMapView! I can't access any of the specialty code within the open class.
and if I conform it to the open class I don't know how to use my tap gesture.
I'm supposed to call this code below in whatever VC I want the map to appear in:
super.viewDidLoad()
let map = MapViewController(frame: view.frame)
map.set(api: api)
map.set(center: "school.truck.lunch", latitudeSpan: 0.05, longitudeSpan: 0.05)
map.showsUserLocation = true
map.onError = { error in
self.showError(error: error)
}
self.view = map
}
// MARK: Show an Error
/// display an error using a UIAlertController, error messages conform to CustomStringConvertible
func showError(error: Error) {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Error", message: String(describing: error), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
Any idea how I can get a tap gesture to work while still using this special class? I feel like I've tried everything. UGH!! Please assist.
The simplest way to do it is to add the following to your ViewController:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let location = touches.first?.location(in: map) {
map.screenCoordsToWords(point: location) { square in
print(square.words ?? "")
}
}
}
And in your MapView class add this:
func screenCoordsToWords(point: CGPoint, completion: #escaping (W3WSquare) -> () ) {
let coordinates = convert(point, toCoordinateFrom: self)
self.api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
completion(s)
}
}
}
But doing it this way can lead to confusion between MKMapView's touch event handler and your view controller. Depending on what you're trying to accomplish this might be okay, or not okay.
If it's an issue, you can instead attach a gesture recognizer inside your MapView, but there is a trick to prevent overriding the MKMapView's built in double tap recogniser:
func addGesture() {
/// when the user taps the map this is called and it gets the square info
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
// A kind of tricky thing to make sure double tap doesn't trigger single tap in MKMapView
let doubleTap = UITapGestureRecognizer(target: self, action:nil)
doubleTap.numberOfTapsRequired = 2
addGestureRecognizer(doubleTap)
tap.require(toFail: doubleTap)
tap.delegate = self
addGestureRecognizer(tap)
}
#objc func tapped(_ gestureRecognizer : UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
let coordinates = convert(location, toCoordinateFrom: self)
api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
print(s.words ?? "")
}
}
}
Then in your ViewController's viewDidLoad, set it up:
map.addGesture()

Changing Scenes In Spritekit, Detecting Touches on Buttons

I am attempting to change from a home scene to a game scene in Spritekit. When I run it in the simulator, there is no transition to the GameScene. I also added a print command to see if the touches on the play button were even being registered, and they were not. Here's the code. Thanks!
import SpriteKit
class HomeScene: SKScene {
var playButton: SKSpriteNode?
var gameScene: SKScene!
override func didMove(to view: SKView) {
playButton = self.childNode(withName: "startButton") as? SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let pos = touch.location(in: self)
let node = self.atPoint(pos)
if node == playButton {
let transition = SKTransition.fade(withDuration: 1)
gameScene = SKScene(fileNamed: "GameScene")
gameScene.scaleMode = .aspectFit
self.view?.presentScene(gameScene, transition: transition)
print("touch")
}
}
}
}
The touches parameter inside of your touchesBegan(_: with:) method is of type Set<UITouch>, i.e. a set of touches. As stated in the Apple Documentation for Sets, a set is used when the order of the items in the collection are not a concern. Therefore, you cannot assume that the first item in touches is the first touch in the set. Knowing this, I recommend refactoring your code inside of HomeScene.swift to the following:
import SpriteKit
class HomeScene: SKScene {
private var playButton: SKSpriteNode?
override func sceneDidLoad() {
self.playButton = self.childNode(withName: "//starButton") as? SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
self.touchDown(atPoint: t.location(in: self))
}
}
func touchDown(atPoint pos: CGPoint) {
let nodes = self.nodes(at: pos)
if let button = playButton {
if nodes.contains(button) {
let gameScene = SKScene(fileNamed: "GameScene")
self.view?.presentScene(gameScene)
}
}
}
The touchDown(atPoint:) method does all the heavy lifting and touchesBegan(_: with:) is freed up to listen for touch events. Another thing to note when calling childNode(withName:):
Adding / before the name of the file starts searching for nodes beginning at the root of your file hierarchy.
Adding // before the name of the file starts searching for nodes beginning at the root of your file hierarchy, then recursively down the hierarchy.
Adding * before the name of the file acts as a character wildcard and allows you to search for file names that are wholly or partially unknown.
I think you should use the node's name instead of the node itself for comparison in the touchesBegan... try something like this:
import SpriteKit
class HomeScene: SKScene {
var playButton: SKSpriteNode?
var gameScene: SKScene!
override func didMove(to view: SKView) {
playButton = self.childNode(withName: "startButton") as? SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let pos = touch.location(in: self)
let node = self.atPoint(pos)
if node.name == "startButton" {
let transition = SKTransition.fade(withDuration: 1)
gameScene = SKScene(fileNamed: "GameScene")
gameScene.scaleMode = .aspectFit
self.view?.presentScene(gameScene, transition: transition)
print("touch")
}
}
}
}
Hope it helps! :)

Button is still working even when removeFromParent() in Swift SpriteKit is called

In the code below if I press the removeButton the button would disappear but it's still working. Maybe I should make the if statement false but I don't know how.
class GameScene : SKScene {
var button : SKSpriteNode!
var removeButton : SKSpriteNode!
var image : SKSpriteNode!
override func didMove(to view: SKView) {
createButton()
createRemoveButton()
}
func createButton() {
button = SKSpriteNode(imageNamed: "button")
button.position = CGPoint(x: -300,y: 0)
self.addChild(button)
}
func createRemoveButton() {
removeButton = SKSpriteNode(imageNamed: "removeButton")
removeButton.position = CGPoint(x: 300,y: 0)
self.addChild(removeButton)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
if button.contains(touchLocation) {
image = SKSpriteNode(imageNamed: "image")
image.position = CGPoint(x: 0,y: 300)
self.addChild(image)
}
if removeButton.contains(touchLocation) {
button.removeFromParent()
}
}
override func update(_ currentTime: TimeInterval) {
}
}
You need to understand ARC (Automatic Reference Counting) and what it takes to retain an instance.
In your case you are retaining button, which means it does not delete from memory.
Now when you remove the button from the parent, the button's frame is still exactly the same, the only thing different is button.parent is now nil
When you call button.contains(touchLocation), this is going to pass because you are not concerned with whether or not button is on the scene, all you are doing is checking if the touchLocation is within the frame of the button.
The quickest fix is to check the parent:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
if button.parent == self && button.contains(touchLocation) {
image = SKSpriteNode(imageNamed: "image")
image.position = CGPoint(x: 0,y: 300)
self.addChild(image)
}
if removeButton.contains(touchLocation) {
button.removeFromParent()
}
}
In reality though, you need to learn how to better manage your resources. I would recommend trying to find tutorials about how ARC works.
It is normal because you dont use the good button. You use button instead of removeButton.
You should do:
if removeButton.contains(touchLocation) {
removeButton.removeFromParent()
}
Now it should work correctly

How to change button images from another class?

I´m new to coding and I want to know how to change the buton image in the menu class from the setting class.
Here is the menu class:
class Mainmenu: SKScene{
var ratingButton = SKSpriteNode(imageNamed: "RatingButton1")
}
In my setting class I want to change this image to "RatingButton2" just by clicking a button.
Here is the setting class:
class Settings: SKScene {
override func didMove(to view: SKView) {
self.backgroundColor = SKColor.white
let DCButton = SKSpriteNode(imageNamed: "ChangeButton")
DCButton.position = CGPoint(x: self.size.width * 0.2, y: self.size.height * 0.8)
DCButton.setScale(0.53)
self.addChild(DCButton)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let locationUser = touch.location(in: self)
if atPoint(locationUser) == DCButton {
//Change the button image here
}
}
}
}
Swift 3+ Use Notificationcenter to update your button image.
// Define identifier
let notificationName = Notification.Name("NotificationIdentifier")
// Register to receive notification in Mainmenu
NotificationCenter.default.addObserver(self, selector: #selector(receivedNotification), name: notificationName, object: nil)
// Post notification into Settings write this line into "touchesBegan" method
NotificationCenter.default.post(name: notificationName, object: nil)
// Stop listening notification if required.
NotificationCenter.default.removeObserver(self, name: notificationName, object: nil)
Notification Handler
func receivedNotification(notification: Notification){
// Set your button image here...
}
I would not recommend using NSNotificationCenter for this task, it adds a lot of overhead for something that will be rarely used.
Now unfortunately I do not know how your layout is structured, so I am not sure what answer is best for you.
If Menu and Settings are both on the scene at the same time, then you can search the scene to find the button you are looking for:
class Mainmenu: SKScene{
var ratingButton = SKSpriteNode(imageNamed: "RatingButton1")
ratingButton.name = "ratingButton"
}
class Settings: SKScene {
override func didMove(to view: SKView) {
self.backgroundColor = SKColor.white
let DCButton = SKSpriteNode(imageNamed: "ChangeButton")
DCButton.position = CGPoint(x: self.size.width * 0.2, y: self.size.height * 0.8)
DCButton.setScale(0.53)
self.addChild(DCButton)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let locationUser = touch.location(in: self)
if atPoint(locationUser) == DCButton {
let button = scene.childNode(withName:"//ratingButton") as! SKSpriteNode
button.texture = SKTexture(imageNamed:"RatingButton2")
}
}
}
}
If it is not on the scene and you are presenting it, then you want to use userData to assign what your button is before hand
let menu = Mainmenu(...)
menu.userData = ["ratingButton":SKTexture(imageNamed:textureName)] //texture name is either Ratingbutton1 or RatingButton2 depending on state
self.view.presentScene(menu)
Then in the viewDidLoad, just read the userData
func viewDidLoad(...)
{
ratingButton.texture = userData?["ratingButton"]
}

Replay button doesn't work. (Swift, SpriteKit)

Once my game ends, it displays a button for replay, but I can't understand how to let Xcode know if there is a touch after the game has ended. My replay button's code is in didBeginContact Method and is as follows:
func didBeginContact(contact: SKPhysicsContact) {
if (.....) {
..........
} else {
replayButton = SKSpriteNode(imageNamed: "ReplayButton")
replayButton.position = CGPoint(x: size.width / 1.75, y: size.height / 2.5)
replayButton.name = "replayButton"
self.addChild(replayButton)
}
New Swift file:
class button: SKSprideNode {
let replayButton = button(imageNamed: "replayButton")
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
var touch = touches as! Set<UITouch>
var location = touch.first!.locationInNode(self)
var node = self.nodeAtPoint(location)
if (node.name == "replayButton") {
let myScene = EMode(size: self.size)
let reveal = SKTransition.fadeWithDuration(2.0)
self.scene!.view(myScene, transition: reveal) //error on this line
}
}
}
You need to set the replay button's user interaction enabled to true like this:
replayButton.userInteractionEnabled = true
Then you just need to listen for when the button or scene is touched. One way to do that is make a subclass of SKSpriteNode called Button or something, and override the touchesBegan method.
EXAMPLE:
ButtonClass
class ExampleButton: SKSpriteNode {
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
//do stuff here
}
}
Button Initialization
let button = ExampleButton(imageNamed: "ButtonImage")
button.userInteractionEnabled = true