My app creates several SKNodes and SKSpriteNodes dynamically based on incoming events. When I try to cleanup obsolete nodes and their contained sub-nodes I use node.removeFromParent().
However as soon as I try to use the cleanup code using deinit's on all node layers I immediately get EXC_BAD_ACCESS.
The most safe version I think should be the following piece of code and it badly fails with the mentioned exception as well:
deinit {
if self.child1.parent!.children.contains(self.child1) {
self.child1.removeFromParent()
}
}
So to sum it up: the child node has still a parent (self.child.parent != nil). Even finding child1 in the list of the children of its parent says: hey I'm still there and po child1 and po child1.parent! shows valid node objects. But still removeFromParent() makes the app crash.
Log messages in deinit() show that the deinitializers are only called once per object...
I was able to reproduce it in a small project. The essential things should be contained in those two classes:
import SpriteKit
public class ContainerNode : SKNode {
private let myNode : CustomNode
override init() {
self.myNode = CustomNode()
super.init()
self.addChild(self.myNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.myNode.removeFromParent()
self.removeFromParent()
}
}
public class CustomNode : SKNode {
private let containerNode : SKNode
private let child1 : SKNode
override public init() {
self.child1 = SKNode()
self.containerNode = SKNode()
super.init()
self.containerNode.addChild(self.child1)
self.addChild(self.containerNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if self.child1.parent!.children.contains(self.child1) {
self.child1.removeFromParent() // EXC_BAD_ACCESS here !!
}
self.containerNode.removeFromParent()
self.removeFromParent()
}
}
class GameScene: SKScene {
private var myNodes = [ContainerNode]()
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if myNodes.count >= 5 {
self.removeAllChildren()
self.myNodes.removeAll()
}
for touch in touches {
let location = touch.locationInNode(self)
let node = ContainerNode()
node.position = location
myNodes.append(node)
self.addChild(node)
}
}
}
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let scene = GameScene(fileNamed:"GameScene") {
UIApplication.sharedApplication().idleTimerDisabled = true
let skView = self.view as! SKView
skView.multipleTouchEnabled = true;
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
The code can be pasted into the standard Spaceship demo (it loads the gamescene file of the demo). Click 6 times on the background. The first 5 clicks add nodes to the scene. On the 6th click all nodes are going to be removed programmatically then which should make the app crash.
Any ideas appreciated. Is this an Apple bug or did I miss something ?
XCode 7 GM, crash in Simulator and on IPhone5...
I eventually found the reason for the crash.
SpriteKit simply is not thread-safe and I was using it in a multi-threaded way (basic rendering loop plus a separate thread receiving network packets which in the same thread updated the scene graph of SpriteKit too). The unpredictable nature of incoming network packets caused the sporadic and hard to reproduce crashes.
Bug report at Apple was answered with the same explanation:
Don't update the scene graph from multiple threads at the same time.