Deallocate SKScene after transition to another SKScene in SpriteKit

I am just starting out with SpriteKit in Swift and I had the same problem: My intro scene was playing some music that I wanted to stop after transitioning to the main menu scene, and I figured the deinit would be a good place to put the AVAudioPlayer.stop() call in, but deinit was never being called.

After looking around I learned that it may be because of some strong references to the scene, so in my GameViewController:UIViewController subclass I changed this code:

let intro = IntroScene(size: skView.bounds.size)
skView.presentScene(intro)

to

skView.presentScene(IntroScene(size: skView.bounds.size))

and in the intro scene, that I wanted to be deallocated, I changed

let mainMenu  = MainMenuScene(size: self.size)
let crossFade = SKTransition.crossFadeWithDuration(1)

self.scene.view.presentScene(mainMenu, transition: crossFade)

to

self.scene.view.presentScene(MainMenuScene(size: self.size),
                             transition: SKTransition.crossFadeWithDuration(1))

and it worked! After the transition was complete the deinit method got called.

I assume that the outgoing scene was not being deinitialized because there were variables holding references to it.


There's nothing wrong with SKScene or SKView, as long as I can see. Make sure the scene instance is not strongly reference anywhere else, especially inside a block. Blocks are highly probable to be ignored.

More about weak reference inside a block: https://stackoverflow.com/a/17105368/571489

As far as I see, you do have a block strongly referencing the scene instance:

[SKTexture preloadTextures:to_preload withCompletionHandler:^{
    [self fadeRemove:masterOverlay];
    [self initialize];
}];

December 2019/Swift 5

Update:

My layout:

I have a single view controller that contains 2 SKViews which each of them have their own unique SKScene that are presented at the same time. One SKView & its SKScene is the main overworld where the player character is rendered, controlled, NPC's rendered, camera tracking, the whole shebang etc., & the other SKView & its SKScene display the mini map of the overworld. You can imagine there are also quite a number of SKSpriteNode's & lot of them ALWAYS have some kind of SKAction/animation running non-stop (swaying trees for instance). My SKScenes even contain their own arrays pointing at specific groups of SKSpriteNodes, such as, character nodes, building nodes, tree nodes. This is for quick access & convenience purposes. Plus, I have a few singletons that either contain an array of SKTextures, or character models, etc.. They are kept around as an optimization for quick data access rather than reading from disc/accessing storage every time I need something. There are even UIKit elements used for the game UI inside the view controller. Many other objects, such as, my models that contain data on characters, buildings, the entire game session all have some kind of delegates pointing at someone. On top of all of this the codebase is massive.

After observing memory in the debug session I found out the sure-fire way to make sure nothing is retained is to absolutely make sure the following:

Memory handling:

  • Pass in nil for scene presentation & set that skview's property/pointer to nil as well
  • Remove all view controller primary view subviews
  • Absolutely set every delegate you have to nil !!!!!!!!!!!!!!
  • Remove all observers IF you happen to have any
  • Anywhere you have an array/dictionary or pointer to some kind of object YOU created, set it to nil/empty it
  • CRITICAL: put all of the above into a single function/method & make sure to call it right before changing view controllers!

*NOTE: IF you have 1 view controller for the entire app (congrats on squeezing everything - youz overlord squeezer), then do NOT nil everything & use caution. BUT the previous scene still needs to be set to nil during presentation.

So something like this if you're jumping between view controllers:

/// Remove all pointers to any data, nodes & views.
fileprivate func cleanUp() {
    guard self.skView != nil else { return }
    // Session was passing data updates to this view controller; time to nil it
    self.gameSessionModel.delegate = nil
    self.skView.presentScene(nil)
    self.skViewMiniMap.presentScene(nil)
    self.skView = nil
    self.skViewMiniMap = nil
    for subview in self.view.subviews {
        subview.removeFromSuperview()
    }
}

/// Take the user back to the main menu module/view controller.
fileprivate func handleMenuButton() {
    // First, clean up everything
    self.cleanUp()
    // Then go to the other view controller
    let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MainViewController")
    self.show(view, sender: .none)
}

Call handleMenuButton() function or whatever function you use to present another view controller, but make sure the cleanUp() function is called within it! *NOTE: Classes such as 'gameSessionModel' are entirely my own custom classes. Xcode will throw an error on such things (unless you magically have the same one...) so delete such things. They are only used as example code in case you have delegates pointing at the current view controller.

If you're only presenting different SKScene's with a single SKView, then your cleanUp() function can end up being "lighter" as such:

/// Remove all pointers to any data, nodes & views.
fileprivate func cleanUp() {
    self.skView.presentScene(nil)
    // Previous scene was set to nil, its deinit called. Now onto the new scene:
    let gameScene = self.setupGameScene(id: "perhapsYouUseStringIDsToDisntiguishBetweenScenes?")
    self.skView.presentScene(gameScene)
}

*NOTE: Do NOT forget to use the SKScene's deinit method to remove anything you won't need. It's a practice I use all the time for all my classes to untangle anything I might have missed.


A similar problem was faced by the person who asked this question.

When asked whether he was able to solve it, they said:

Yes, I did, there wasn't anything I could do about it from the scene or Sprite Kit for that matter, I simply needed to remove the scene and the view containing it completely from the parent view, cut all its bonds to the other parts of the system, in order for the memory to be deallocated as well.

You should use separate views for each of your scene and transition between these views. You can follow these steps to make it look natural:

1 - At the point you want to transition from one scene to the other, take a snapshot of the scene using the following code:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, scale);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Then, add this image as a subview of the SKView of the current scene, and remove the scene.

2 - Initialise the new scene on another view, and transition between the two views using UIView animation.

3 - Remove the first view from it's superview.