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))
}

Get the Swift Dev Journal Newsletter

Subscribe and get exclusive articles, a free guide on moving from tutorials to making your first app, notices of sales on books, and anything I decide to add in the future.

    We won't send you spam. Unsubscribe at any time.