How to present SKScene from UIViewController in swift - swift

My SpriteKit app (named CurlShots) is crashing at launch when it is trying to present the first scene (the main menu). Everything works fine in simulator and devices with debug builds but with an archived build that I'm installing with iTunes I'm getting a crash in didMoveToView of the menu scene.
#objc class MenuScene: SKScene {
...
Last Exception Backtrace:
0 CoreFoundation 0x18673c2d8 __exceptionPreprocess + 132
1 libobjc.A.dylib 0x197be80e4 objc_exception_throw + 60
2 CoreFoundation 0x186743290 +[NSObject(NSObject) doesNotRecognizeSelector:] + 220
3 CoreFoundation 0x186740154 ___forwarding___ + 928
4 CoreFoundation 0x186642ccc _CF_forwarding_prep_0 + 92
5 CurlShots 0x10011b208 function signature specialization of CurlShots.MenuScene.didMoveToView (CurlShots.MenuScene)(ObjectiveC.SKView) -> () (MenuScene.swift:71)
6 CurlShots 0x100116c80 #objc CurlShots.MenuScene.didMoveToView (CurlShots.MenuScene)(ObjectiveC.SKView) -> () (MenuScene.swift:0)
7 SpriteKit 0x18afd7770 -[SKScene _didMoveToView:] + 88
8 SpriteKit 0x18afef004 -[SKView presentScene:] + 264
9 CurlShots 0x10012801c function signature specialization of CurlShots.GameViewController.viewWillLayoutSubviews (CurlShots.GameViewController)() -> () (GameViewController.swift:70)
I'm using auto layout and viewDidLoad of the VC has no code. Instead, I'm presenting the scene from viewWillLayoutSubviews because I need the device screen size for initializing the scene
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let skView = self.view as! SKView
if skView.scene == nil
{
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
let mainMenu = MenuScene(size: skView.bounds.size)
mainMenu.scaleMode = .AspectFill
skView.presentScene(mainMenu)
}
}
I've tried to remove all code from didMoveToView of the menu scene but that has no effect. Also tried commenting out variables in the menu scene although none of them were conditional. I tried to retain the mainMenu scene in a variable to avoid it being released but that did not solve the crash. Also tried overriding the init(size) of the scene to verify that the menu scene actually gets created before it is presented.
Don't know what else to try. Because the crash only happens in archived package I cannot debug by setting breakpoints.

Did you make sure to set the Main Menu as your first view by going to story board and on the right panel selecting "set view as first view" ( or something like that )? sometimes this throws this error. May not be the overall solution but it is worth checking

I found a solution to the problem by meticulously removing code lines until I found the root cause. This looks more like a bug on the Apple side but anyways changing one code line fixed the crash.
The crash was caused by the scene to be presented. Inside didMoveToView I was checking whether the device is an iPad by using UIUserInterfaceIdiom and for some reason swift did not like that in release build. I'm calculating a scaling factor per the screen width in the code so now I use that info to determine whether the device is an iPad. So, the code change that I did was to replace line
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.Pad) {
with a line
if(scaling > 2.0) { // if iPad

Related

SpriteKit SKScene not resizing correctly to fit iPhone 12

I've created a new SpriteKit project and want the scene to fill the screen entirely. I have defined the GameScene with iPhone 12 screen dimensions (390pt x 844pt) and specified the following in GameViewController.swift:
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "GameScene") {
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
}
However, there is still a large amount of unoccupied space above and below the SKScene. I can't figure out what I've done wrong, and any help would be appreciated.
Fix (credit #aheze)
As mentioned here, this can be fixed by setting the launch screen file in your project settings under App Icons and Launch Images.

SpriteKit Memory leaks changing Scenes containing SKTileMapNodes

I am trying to create a simple 2d platform game using Swift, SpriteKit, and SKTileMaps. But every time i change between scenes containing SKTileMaps I see a lot of memory leaks in the Xcode Instruments.
I have recreated the problem as simple as I can. I am using a .sks file to create the scene and this file only contains 1 tileMap filled with some tiles.
The code in the view controller for presenting the scene:
if let view = self.view as! SKView? {
let scene = LoadingScene(size: CGSize(width: 2048, height: 1536))
scene.scaleMode = .aspectFill
view.presentScene(scene)
The code for the scene:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let scene = GameScene(fileNamed: "WorldScene") else{fatalError("Could not open world scene")}
view?.presentScene(scene)
}
}
I am choosing GameScene as the custom class int the .sks scene file.
This will result in a lot of small memory leaks each time I change the scene:
picture of memory leak in Instruments
This is the leak from only one scene change. Am I doing something wrong or is this a SpriteKit bug?
Edit1
The SKCTileMapNode::_ensureChunkForTileIndex(unsigned int) leaks happen each time i load the tile map, while the rest only appear when changing a scene
Edit2
Changed the GameViewController to skip the LoadingScene and go straight to GameScene. The memory leak is still there:
if let view = self.view as! SKView? {
guard let scene = GameScene(fileNamed: "WorldScene") else{fatalError("Could not open world scene")}
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
I have come across this same issue and after looking into it, I believe it is a problem that is inherent to all SKTileMapNodes. It is a bug in SpriteKit.
When you use an SKTileMapNode with ANY tiles filled (not a blank tile map), then the memory for the tile map will persist even when loading subsequent scenes. If you keep loading levels with SKTileMapNodes, then the memory will keep increasing until the game ultimately crashes. I have tested this with different games I have coded and using the code of others as well.
Interestingly, if the tile map has all blank tiles (even if it has an SKTileSet assigned), then no memory leak will occur.
From the best that I can guess, when an SKTileMapNode has any tile in it besides blank tiles, the entire SKTileSet that it is using, is being kept in memory and never gets removed.
From my experiments, you can prevent this problem by swapping the SKTileSet with a blank tileset in didMove. You cannot do it anywhere else except within didMove (or a function called by didMove).
So my solution has been to extract the tile set into individual sprites and then "nullify" the tileset on the tilemap. You can do so with the following code:
extension SKTileMapNode {
func extractSprites(to curScene: SKScene) {
for col in 0..<numberOfColumns {
for row in 0..<numberOfRows {
if tileDefinition(atColumn: col, row: row) != nil {
let tileDef = tileDefinition(atColumn: col, row: row)
let newSprite = SKSpriteNode(texture: tileDef!.textures.first!)
curScene.addChild(newSprite)
let tempPos = centerOfTile(atColumn: col, row: row)
newSprite.position = convert(tempPos, to: curScene)
}
}
}
eraseTileSet()
}
func eraseTileSet() {
let blankGroup: [SKTileGroup] = []
let blankTileSet = SKTileSet(tileGroups: blankGroup)
tileSet = blankTileSet
}
}
Basically within didMove, you will need to call extractSprites on each SKTileMapNode. This will simply create SKSpriteNodes from each tile and put them into the scene. Then the SKTileSet will be "switched" to a blank one. Magically the memory leak will disappear.
This is a simplified solution which you will need to expand upon. This only puts the sprites there, but doesn't define how they behave. Sorry but this is the only solution I have found and I believe your problem is a major bug in SpriteKit.

SpriteNode disappears when returning to GameScene

I have a background sprite that I display in my GameScene. When I go to another scene (BuildViewController) and come back by background is gone. I'm sure it is a simple fix and it isn't anymore complicated than what I have said above. Heres some code :)
while length <= 6 {
while wide <= 6 {
let imageViewBackground = UIImageView(frame: CGRect(x:0 + (wide*100), y:0 + (length*200), width: 100, height: 200))
imageViewBackground.image = UIImage(named: "grass.png")
self.view?.addSubview(imageViewBackground)
self.view?.sendSubview(toBack: imageViewBackground)
wide = wide + 1
}
wide = 0
length = length + 1
}
You may be finding that by adding your imageView to the view controller along with the SKScene that things are drawing all oddly (it could also be that your imageView is getting dealloc'd but I can't say based on your code).
I'm assuming you're using the basic Xcode template to set up your views and stuff; so to make the grass, try something like this in your GameScene class instead of the code above (which I assume is in your GameViewController class). You can put the code in didMove(to view: SKView) or you can add a sceneDidLoad() func. Make a SKSpriteNode for the grass and tile it like you are doing already. The two methods I mention here are the ones most closely related to the viewDidLoad and viewWillAppear methods in UIViewController land.
while length <= 6 {
while wide <= 6 {
let grassSprite = SKSpriteNode(imageNamed: "grass.png")
grassSprite.position = CGPoint(x: (wide * 100), y: (length * 200))
self.addChild(grassSprite
wide += 1
}
wide = 0
length += 1
}
All of this being said, it appears you are trying to make a grass background for something, in which case, you should probably look into SKTileGroup which has some nice conveniences.

SpriteKit not deallocating all used memory

I have ready many (if not all) articles on SO and other sites about the disasters of dealing with SpriteKit and memory issues. My problem, as many others have had, is after i leave my SpriteKit scene barely any of the memory added during the scene session is released. I've tried to implement all suggested solutions in the articles i've found, including, but not limited to...
1) Confirm the deinit method is called in the SKScene class.
2) Confirm no strong references to the parent VC in the scene class.
3) Forcefully remove all children and actions, and set the scene to nil when the VC disappears. (Setting the scene to nil was what got the deinit method to eventually get called)
However, after all of that, memory still exists. Some background, this app goes between standard UIKit view controllers and a SpriteKit scene (it's a professional drawing app). As an example, the app is using around 400 MB before entering a SpriteKit scene. After entering the scene and creating multiple nodes, the memory grows to over 1 GB (all fine so far). When i leave the scene, the memory drops maybe 100 MB. And if i re-enter the scene, it continues to pile on. Are there any ways or suggestions on how to completely free all memory that was used during a SpriteKit session? Below is a few of the methods being used to try and fix this.
SKScene class
func cleanScene() {
if let s = self.view?.scene {
NotificationCenter.default.removeObserver(self)
self.children
.forEach {
$0.removeAllActions()
$0.removeAllChildren()
$0.removeFromParent()
}
s.removeAllActions()
s.removeAllChildren()
s.removeFromParent()
}
}
override func willMove(from view: SKView) {
cleanScene()
self.removeAllActions()
self.removeAllChildren()
}
Presenting VC
var scene: DrawingScene?
override func viewDidLoad(){
let skView = self.view as! SKView
skView.ignoresSiblingOrder = true
scene = DrawingScene(size: skView.frame.size)
scene?.scaleMode = .aspectFill
scene?.backgroundColor = UIColor.white
drawingNameLabel.text = self.currentDrawing?.name!
scene?.currentDrawing = self.currentDrawing!
scene?.drawingViewManager = self
skView.presentScene(scene)
}
override func viewDidDisappear(_ animated: Bool) {
if let view = self.view as? SKView{
self.scene = nil //This is the line that actually got the scene to call denit.
view.presentScene(nil)
}
}
As discussed in the comments, the problem is probably related to a strong reference cycle.
Next steps
Recreate a simple game where the scene is properly deallocated but some of the nodes are not.
I'll reload the scene several time. You'll see the scene is properly deallocated but some nodes into the scene are not. This will cause a bigger memory consumption each time we replace the old scene with a new one.
I'll show you how to find the origin of the problem with Instruments
And finally I'll show you how to fix the problem.
1. Let's create a game with a memory problem
Let's just create a new game with Xcode based on SpriteKit.
We need to create a new file Enemy.swift with the following content
import SpriteKit
class Enemy: SKNode {
private let data = Array(0...1_000_000) // just to make the node more memory consuming
var friend: Enemy?
override init() {
super.init()
print("Enemy init")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Enemy deinit")
}
}
We also need to replace the content of Scene.swift with the following source code
import SpriteKit
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size: size)
print("Scene init")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("Scene init")
}
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
addChild(enemy0)
addChild(enemy1)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let newScene = GameScene(size: self.size)
self.view?.presentScene(newScene)
}
deinit {
print("Scene deinit")
}
}
As you can see the game is designed to replace the current scene with a new one each time the user taps the screen.
Let's start the game and look at the console. Will' see
Scene init
Enemy init
Enemy init
It means we have a total of 3 nodes.
Now let's tap on the screen and let's look again at the console
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
Enemy deinit
Enemy deinit
We can see that a new scene and 2 new enemies have been created (lines 4, 5, 6). Finally the old scene is deallocated (line 7) and the 2 old enemies are deallocated (lines 8 and 9).
So we still have 3 nodes in memory. And this is good, we don't have memory leeks.
If we monitor the memory consumption with Xcode we can verify that there is no increase in the memory requirements each time we restart the scene.
2. Let create a strong reference cycle
We can update the didMove method in Scene.swift like follows
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
// ☠️☠️☠️ this is a scary strong retain cycle ☠️☠️☠️
enemy0.friend = enemy1
enemy1.friend = enemy0
// **************************************************
addChild(enemy0)
addChild(enemy1)
}
As you can see we now have a strong cycle between enemy0 and enemy1.
Let's run the game again.
If now we tap on the screen and the look at the console we'll see
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
As you can see the Scene is deallocated but the Enemy(s) are no longer removed from memory.
Let's look at Xcode Memory Report
Now the memory consumption goes up every time we replace the old scene with a new one.
3. Finding the issue with Instruments
Of course we know exactly where the problem is (we added the strong retain cycles 1 minute ago). But how could we detect a strong retain cycle in a big project?
Let click on the Instrument button in Xcode (while the game is running into the Simulator).
And let's click on Transfer on the next dialog.
Now we need to select the Leak Checks
Good, at this point as soon as a leak is detected, it will appear in the bottom of Instruments.
4. Let's make the leak happen
Back to the simulator and tap again. The scene will be replaced again.
Go back to Instruments, wait a few seconds and...
Here it is our leak.
Let's expand it.
Instruments is telling us exactly that 8 objects of type Enemy have been leaked.
We can also select the view Cycles and Root and Instrument will show us this
That's our strong retain cycle!
Specifically Instrument is showing 4 Strong Retain Cycles (with a total of 8 Enemy(s) leaked because I tapped the screen of the simulator 4 times).
5. Fixing the problem
Now that we know the problem is the Enemy class, we can go back to our project and fix the issue.
We can simply make the friend property weak.
Let's update the Enemy class.
class Enemy: SKNode {
private let data = Array(0...1_000_000)
weak var friend: Enemy?
...
We can check again to verify the problem is gone.

Unable to pass a variable to a scene in spritekit in swift

I'm currently having trouble with passing variables to a scene in spritekit (swift). In android when moving to a different activity, I just set the parameter to the Intent and simply get the variable. But since I'm new to spritekit and swift, i find it difficult. Is there a clear way to pass a variable to the scene ? I have a sample code below that I tested but did not work.
class GameSKView: SKView {
var mylevel = 0
}
//Inside my main GameScen
let gameView = self.view as GameSKView
sample.text = "my level is :\(gameView.mylevel)"
Try this:
class GameSKView: SKView {
internal var mylevel = 0
}
And inside you main game scene
if let gameView = self.view as GameSKView {
sample.text = "my level is :\(gameView!.mylevel)"
}
Finally found the solution.
In order to use the custom SKView, I have done the following procedures.
Open the Idenity inspector in the Main.storyboard
Replace the [SKView] inside the Custom Class with your [customSKView]
, In my case GameSKView
Save. That was all I needed to do.