Scene Editor Development: Saving with NSKeyedArchiver
In the last article, I wrote about how I couldn’t use the PropertyListEncoder
class to save scenes because SpriteKit’s classes don’t conform to the Codable
protocol. I have to use NSKeyedArchiver
to save the scenes, and that’s what I’m going to write about in this article.
Using NSKeyedArchiver
Archiving data with NSKeyedArchiver
requires one function call. Call the class function archivedData
. Supply a root object and whether or not the archive requires secure coding. You should set the second argument to true. I used the following initial code to archive a scene:
let sceneData = try NSKeyedArchiver.archivedData(withRootObject:
GameScene.self, requiringSecureCoding: true)
Calling the archivedData
function is enough in most cases. But I also had to say that my SKScene
subclass supports secure coding.
override static var supportsSecureCoding: Bool {
get {
return true
}
}
Problem 1: Unrecognized Selector Error
When I saved the scene, the app would crash with the following error message in Xcode’s console:
+[SpriteKit_Editor.GameScene encodeWithCoder:]: unrecognized selector sent to class 0x10cb6aa60
The error message is saying that my GameScene
class does not have an encodeWithCoder
function. The cause of the error is I was passing my SKScene
class as the root object instead of the scene itself. Passing the scene as the root object fixed the error.
let sceneData = try NSKeyedArchiver.archivedData(withRootObject:
scene, requiringSecureCoding: true)
Problem 2: Root Object Has the Wrong Class
I wanted to see if I could load a scene I saved in a SpriteKit game project. I created a test project in Xcode, added a scene I created in the editor, and tried to load the scene with the following code:
currentScene = SKScene(fileNamed: "TestGameScene")!
The app crashed with the following error message in Xcode’s console:
Terminating app due to uncaught exception ‘NSInvalidUnarchiveOperationException’, reason: ‘*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (SpriteKit_Editor.GameScene) for key (root) because no class named “SpriteKit_Editor.GameScene” was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target). If the class was renamed, use setClassName:forClass: to add a class translation mapping to NSKeyedUnarchiver’
The error message says the type of the root object in the archive is the SKScene
subclass in the scene editor app when it needs to be SKScene
. I have to archive the scene as type SKScene
.
My first attempt was to cast the scene as type SKScene
.
let sceneData = try NSKeyedArchiver.archivedData(withRootObject:
scene as SKScene, requiringSecureCoding: true)
But I got the same crash when trying to load the scene in a SpriteKit project.
My second attempt was to create a SKScene
variable and archive that variable.
let skScene: SKScene = scene
let sceneData = try NSKeyedArchiver.archivedData(withRootObject:
skScene, requiringSecureCoding: true)
That did not fix the error.
The solution is to call the setClassName
function to tell the archiver to save the scene as type SKScene
. The following code saves the scene correctly:
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
NSKeyedArchiver.setClassName("SKScene", for: GameScene.self)
let sceneData = try NSKeyedArchiver.archivedData(withRootObject:
scene, requiringSecureCoding: true)
return .init(regularFileWithContents: sceneData)
}
Pleasant Surprise: BBEdit Can Show Binary Property Lists
I learned that the text editor BBEdit shows binary property lists as XML files. Now I don’t have to worry about being unable to examine the contents of the scene. I can save the scenes as binary property lists and view them in BBEdit. I can also view the scene in Xcode as a SpriteKit scene.
Unarchiving the Scene
To open scenes in the scene editor, I have to add support for unarchiving the data. To support unarchiving I use the NSKeyedUnarchiver
class. I have to set the class name to treat the SKScene
root object as an object of my subclass. The last step is to call the unarchivedObject
function to read the archived data and create an instance of my subclass from the data. The following code loads the scene document:
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents
else {
throw CocoaError(.fileReadCorruptFile)
}
NSKeyedUnarchiver.setClass(GameScene.self, forClassName: "SKScene")
scene = try NSKeyedUnarchiver.unarchivedObject(ofClass:
GameScene.self, from: data)
?? GameScene(size: CGSize(width: 2048, height: 1536))
}