Deallocate SKScene after modal transition - ios

I have been creating a game in sprite kit using swift and have encountered a problem. I have two view controllers, each with one scene and one transitions to the other modally. This all works perfectly first time round, but then when i return to the first view controller and then go to the second again, i have using double the memory. This gives me the impression that nothing is being deallocated, but the objects are rather reallocated every time I transition to the scene. I ran the app in instruments and got the same result. In the below image i moved from one scene to the next, and then back to the first one again and yet it appears to reallocate the first scene and yet not clear any memory. As the dealloc method is unused now, i don't see how i can fix this. I will post the code to the first view controller below so you can have a look at it. Thanks a lot.
import UIKit
import SpriteKit
class SelectionViewController: UIViewController {
var selectionScene:SelectionScene?
var currentRocketName = ""
#IBOutlet var playButton: UIButton
override func viewDidLoad() {
super.viewDidLoad()
if let selectionScene = SelectionScene.unarchiveFromFile("SelectionScene") as? SelectionScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.multipleTouchEnabled = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
selectionScene.scaleMode = .ResizeFill
selectionScene.viewController = self
skView.presentScene(selectionScene)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "spinnerChanged", name: "spinnerValueChanged", object: nil)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "productBought", name: "ProductBought", object: nil);
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "manageErrorInPurchase", name: "ErrorOccured", object: nil)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func viewDidAppear(animated: Bool) {
}
#IBAction func playButtonPressed(sender: UIButton) {
self.performSegueWithIdentifier("moveToGame", sender: nil)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.toRaw())
} else {
return Int(UIInterfaceOrientationMask.All.toRaw())
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func viewDidUnload() {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "moveToGame" {
let destController = segue.destinationViewController as GameViewController
destController.rocketTexture = SKTexture(imageNamed: self.currentRocketName)
}
}
}
Both selectionScene and currentRocketName are passed to the viewController as soon as they are loaded into the view

I'm not familiar with Swift yet, so I'll give you examples in Objective-C.
Create an IBOutlet for skView. When you are going to present another ViewController, remove skView from it's superview and nil it out:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Need to deallocate GameScene (if game is not paused)
[self.skView removeFromSuperview];
self.skView = nil;
....
}
Don't forget to add skView back to the ViewController's view, when ViewController is getting loaded:
if (!self.skView.window) {
[self.view addSubview:self.skView];
}
To easily check if SKScene was deallocated or not, add this method to it:
- (void)dealloc {
NSLog(#"GAME SCENE DEALLOCATED");
}

I had a similar issue.
Turns out I had created a strong reference by having an SKScene instance as a delegate in another class. After declaring each property of SKScene type or UIView type as weak my issue was resolved:
weak var skScene:SKScene!

Related

Seque from a storyboard image tap into a SpriteKitScene

I'm creating a Swift project for a high school programming class. I can't seem to figure out this problem, and everyone else in my class doesn't seem to have any ideas.
To start, I created a new Swift project, and chose a game format.
I then used some basic code to make the first level for my game, a maze game where the maze moves around instead of the ball based on how the user tilts the device.
This is my GameScene.swift:
import SpriteKit
import CoreMotion
var accelupdateinterval = 0.1
var accelmultiplier = 15.0
class GameScene: SKScene {
let manager = CMMotionManager()
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
I want to have a main menu that the app opens into, which is mainMenu.storyboard:
I successfully have the app launching into the mainMenu.storyboard (and the level physics work well when I've tested the level1.sks), but I'm having trouble figuring out how to segue.
GOAL: I want people to be segued into the level1.sks (and the levels that I add later), when they tap the corresponding image in mainMenu.storyboard.
I can't use the method of adding a Storyboard Reference to segue it, as the Storyboard Reference won't let me choose level1.sks.
I'd also love to find out how to send users back to the main menu when the player icon touches the goal (the blue thing up near the top in this screenshot):
So to do this I think the best approach is to create another ViewController subclass, maybe named LauncherViewController, which will present your SKScene. Then in your storyboard add this viewController and have your menu segue to it on an image press.
Here is a start for the LauncherViewController
class LauncherViewController: UIViewController {
var gameScene: GameScene!
override func viewDidLoad() {
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
}
Where in your menu controller you have a prepareForSegue like this:
override func prepareForSegue(segue: UIStoryBoardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegue" {
let destinationViewController = segue.destinationViewController as! LauncherViewController
destinationViewController.gameScene = GameScene()
}
to have your LauncherViewController dismiss the gameScene when the user finishes the maze, use a delegate pattern. So in GameScene add a protocol above your class
protocol GameDelegate {
func gameFinished()
}
have your LauncherViewController conform to this delegate and set the gameScene's delegate variable to self (see below)
class LauncherViewController: UIViewController, GameDelegate {
var gameScene: GameScene!
override func viewDidLoad() {
gameScene.delegate = self
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
func gameFinished(){
// this forces LauncherViewController to dismiss itself
dismissViewControllerAnimated(true, completion: nil)
}
}
add a variable in GameScene to hold the delegate (LauncherViewController) and add a function that calls the delegate function. You will also need to add the logic to know when the game is over as I haven't done that.
class GameScene: SKScene {
let manager = CMMotionManager()
var delegate: GameDelegate!
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
func gameOver(){
delegate.gameFinished()
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
// it's probably easiest to add the logic for a gameOver here
if gameIsOver {
gameOver()
}
}
}
There will probably be some mistakes in here as I wrote this on my phone so just comment below for anything you are unsure about or doesn't work.

Need help making custom protocol for SKScene

I have no experience with making my own protocols and I am trying to make one so that I can send GameController dpad values to an SKScene. Everything is connected and builds properly, but my SKScene is not receiving any information.
Here is my Main View Controller:
import GameController
import ...
protocol GetsDpadFromController {
func dpadValueUpdate(dpadValue: CGPoint)
}
class GameViewController: UIViewController {
var joystickController: GCController!
var dpadData: GetsDpadFromController? = nil
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "addController:", name: GCControllerDidConnectNotification, object: nil)
}
}
func addController(sender: NSNotificationCenter) {
self.joystickController = GCController.controllers().first
self.joystickController.microGamepad?.reportsAbsoluteDpadValues = true
self.joystickController.microGamepad?.valueChangedHandler = { (gamepad, element) -> Void in
if element == self.joystickController.microGamepad?.dpad {
let x: CGFloat = CGFloat((self.joystickController.microGamepad?.dpad.xAxis.value)!)
let y: CGFloat = CGFloat((self.joystickController.microGamepad?.dpad.yAxis.value)!)
let touchPosition: CGPoint = CGPointMake(x, y)
if let delegate = self.dpadData {
delegate.dpadValueUpdate(touchPosition)
}
}
}
}
And here is my SKScene:
class Page04: SKScene, GetsDpadFromController {
override init(size: CGSize){
super.init(size: size)
}
func dpadValueUpdate(dpadValue: CGPoint) {
print(dpadValue)
}
}
Nothing prints from dpadValueUpdate in the SKScene so I do not get the controller data. I have never made my own protocol before so I'm not too sure what I am doing wrong.
Does anyone know why my SKScene is not receiving data?
You properly declared the GetsDpadFromController protocol and made your Page04 scene conform to it.
The missing part
Looking at your code, the dpadData property in GameViewController will always be nil.
So the body of this IF will never be executed.
if let delegate = self.dpadData {
delegate.dpadValueUpdate(touchPosition)
}
Replace the IF with this
let skView = self.view as! SKView
if let delegate = skView.scene as? GetsDpadFromController {
delegate.dpadValueUpdate(touchPosition)
}
Now you are checking if the current scene does conform to GetsDpadFromController (which will be true for Page04) and if so you are invoking the dpadValueUpdate in your scene.
Let me know if it does work.

Why does my navigation bar disappear from my split view when I rotate on iPhone 6+?

The Problem
I have a problem with my split view. It works fine on iPhone and iPad simulators, but on the iPhone 6+ I lose the navigation bar after rotating the device. Here's what happens on the 6+ simulator:
I start the app and it presents a + button in the navigation bar. I tap this button.
It loads a view controller over the existing view. A navigation bar, as expected, is visible with a working back button.
I turn the device horizontally. As intended the new controller appears in the Master section, with an empty detail section on the right. Unfortunately the navigation bar dissapears.
When I turn the device vertically the navigation bar does not reappear.
In fact when I turn the device horizontally it seems the navigation controller is removed from the stack (I've observed this from outputting the contents of splitViewContoller.viewControllers).
My Code
The test application is simply the Master Detail template with a few modifications.
I've added a new "Add Item" controller and then created a show segue from the Master view's "+" button. The "Add Item" controller is blank, just a blue background.
The DetailViewController has a timerStarted boolean value that is true when the detail view is being used and false when it isn't. The master view is hidden when the detail is in use and displayed when it isn't.
Here's the relevant code (there's nothing interesting in AppDelegate as it's no longer a split view delegate, and MasterViewController has no interaction as the button works via the storyboard)
DetailViewController
import UIKit
class DetailViewController: UIViewController, UISplitViewControllerDelegate {
#IBOutlet weak var detailDescriptionLabel: UILabel!
var collapseDetailViewController = true
var detailItem: AnyObject? {
didSet {
self.configureView()
}
}
var timerStarted: Bool = false {
didSet {
self.changeTimerStatus()
}
}
func configureView() {
if let detail: AnyObject = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
self.timerStarted = true
}
}
}
func changeTimerStatus() {
if self.timerStarted {
if splitViewController!.collapsed == false {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
})
}
collapseDetailViewController = false
} else {
if splitViewController!.collapsed {
self.splitViewController?.viewControllers[0].popToRootViewControllerAnimated(true)
} else {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
})
}
collapseDetailViewController = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
splitViewController?.delegate = self
self.disabledScreen.hidden = false
self.view.bringSubviewToFront(disabledScreen)
self.configureView()
}
override func viewWillAppear(animated: Bool) {
if splitViewController!.collapsed == false && self.timerStarted == false {
splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
}
#IBAction func closeButton(sender: AnyObject) {
self.timerStarted = false
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
if timerStarted == true {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
AddItemViewController
import UIKit
class AddItemViewController: UIViewController, UISplitViewControllerDelegate {
var collapseDetailViewController = false
override func viewDidLoad() {
super.viewDidLoad()
self.splitViewController?.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidAppear(animated: Bool) {
self.splitViewController?.delegate = self
self.collapseDetailViewController = false
}
override func viewWillDisappear(animated: Bool) {
self.splitViewController?.delegate = nil
self.collapseDetailViewController = true
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return self
}
func primaryViewControllerForCollapsingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
I'd be grateful for any suggestions.
I have found the answer. I read a article which I had originally missed because it focuses on changing the detail view rather than the master. As it turns out, the split view works better if I just manage the detail and then the master will take care of itself. Since I never want to change the detail I can simply add the following to my split view delegate:
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
return (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("detailView") as! UIViewController)
}
Once this is done, I no longer lose the navigation bar.

Making a button not hidden in GameScene

Hello my question is simply how to unhide a button in GameScene. I have a segueToMainMenu button that is set up in storyboard.
This is how my GameViewController looks:
#IBOutlet weak var segueToMainMenu: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size: view.bounds.size)
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = false
scene.scaleMode = .AspectFill
skView.presentScene(GameScene(size: skView.bounds.size))
scene.viewController = self
self.segueToMainMenu.hidden = true
}
I set the button to hidden but now in my game I would like to unhide it when a lose func runs in GameScene since when the button is clicked it segues back to the main menu which is a separate view controller also created in the storyboard. Anything helps thank you.
You either can create a delegate for you scene which will call appropriate method on gameOver and do something like
// in the view controller
func gameSceneDidSendGameOver() {
self.segueToMainMenu.hidden = false
}
//in the scene
var gameOverDelegate : GameOverDelegate?
func gameOver() {
gamOverDelegate?.gameSceneDidSendGameOver()
}
Or do it a little bit of ugly way like:
func gameOver() {
let gvc = self.viewController as GameViewController
gvc.segueToMainMenu.hidden = false
}
Edit: Declaration of the delegate protocol.
protocol GameOverDelegate {
func gameSceneDidSendGameOver()
}
class GameViewController : ViewController, GameOverDelegate {
...
func gameSceneDidSendGameOver() {
self.segueToMainMenu.hidden = false
}
}

How to pause SKScene from SKView or ViewController

I have the following code to pause game scene:
class GameProcessScene: SKScene {
...
var onPause: Bool = false {
willSet {
self.paused = newValue
self.view?.paused = newValue
}
}
...
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInNode(self)
let possiblePauseNode = self.nodeAtPoint(location)
if (possiblePauseNode.name == "pauseButton") {
if (self.paused) {
onPause = false
} else {
onPause = true
}
return
}
...
}
}
It works to pause by pressing button. But i also want to pause game when it appears from background. I am trying to do this from ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func pauseGameScene() {
let view = self.view as SKView
let scene = view.scene
switch scene {
case is MainMenuScene:
println("MainMenu")
case let game as GameProcessScene:
game.onPause = true
default:
println("default")
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
But it doesnt work when game appears from background.
Can anybody help? Thanks!
UPD1: Also tried UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification but game.onPause = true still doesnt work. It changes "SKScene.paused" property to true, but the actions are still executing when game enters foreground, even game scene was paused before it goes background.
UPD2: I moved observers and selectors from ViewController class to GameProcessScene: SKScene class - same effect, "paused" property changes, but in fact gameplay is continuing.
I solved my problem by creating a property called isPaused and overriding the paused property to always follow the isPaused property. Note : This disables the SKSCene from automatically pausing when going to background mode. You have to manually set the isPaused property. So only use it in scenes where you actually need the pause mode on resume.
class GameScene: SKScene,SKPhysicsContactDelegate {
// Initializers
var isPaused : Bool = false {
didSet {
self.paused = isPaused
}
}
override var paused : Bool {
get {
return isPaused
}
set {
super.paused = isPaused
}
}
}
In ViewController
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillResignActiveNotification, object: nil)
}
func pauseGameScene() {
println("will become inactive")
let view = self.view as SKView
let scene = view.scene
switch scene {
case let game as GameScene:
game.isPaused = true
default:
println("default")
}
}
Did you try put your code into
override func viewWillAppear(animated: Bool) {
//Put your pauseGameScene(); code here
}
This method called before your view appears every times. The viewDidLoad() run only once after your instance is initialized.

Resources