I am writing an iOS app with SpriteKit but I found conflicting information about removing an SKNode
from its parent which puts me in a deadlock position.
Data races are specifically addressed in the SpriteKit documentation:
If you’re experiencing a segmentation fault or other type of crash occurring deep within the SpriteKit framework, there’s a good chance your code is modifying a SpriteKit object outside of the scene delegate callbacks.
So I understand I should modify (remove) nodes only within these callbacks. In fact I reliably experienced data races/crashes/exceptions when running an SKAction
with a completion block that removes a node on the main queue. Even the SKAction.removeFromParent
action itself throws an exception and becomes essentially unusable.
Calling SKNode.removeFromParent()
in the SKScene
update callback raises an exception and logs a console message that removing a node is only allowed in the main thread. This contradicts the documentation so I smell some forgotten code at Apple.
This is in conflict with another piece of SpriteKit documentation:
All of the SpriteKit callbacks for SKViewDelegate, SKSceneDelegate, and SKScene occur in the main thread and therefore, these are safe places to access or manipulate nodes.
However, I can prove that the SKScene
callbacks are NOT called in the main thread simply by placing a breakpoint in my overridden SKScene.update(:)
method. They are called within some SceneKit rendering serial queue.
In my app I am using SCNSceneRenderer.overlaySKScene
with a SKScene
that complements the SCNScene
. Things presumably work differently in SceneKit, but it’s still a SpriteKit scene.
So what is it now? I cannot remove nodes in the update callback because it’s not called on the main queue. I also cannot remove nodes in the main queue because it causes data races.
Solution #1 is to not remove any nodes at all but this is not really a solution. Some nodes, such as SKShapeNode
, have a fixed frame that can only be set at initialisation time. I could move them to a hidden “graveyard” node or a cache node but this would be just some workaround for something that should work out of the box.
Solution #2 is to prevent data races by pausing the SCNView
, then wait until the last frame finished rendering, and only then remove nodes in the main thread, then unpausing the view again. This is cumbersome and should not be necessary in the first place. And it requires me to find a few still frames (black crossfade transition?) where I can pause the view without annoying users.
Has anyone had this kind of trouble and found any other way?
I have the feeling Apple implemented SCNSceneRenderer.overlaySKScene
with iOS 8 and then made other changes in the coming years and forgot about it.