I have an array of SKSpriteNode
objects that I want to persist to UserDefaults
. In the code below (or see demo project on GitHub), I use NSKeyedUnarchiver
to encode the array as data before setting it to defaults. But when I unarchive the data, the engineSize
property of the objects is reset to the default value of 0.
Car.swift
import SpriteKit
class Car: SKSpriteNode {
var engineSize: Int = 0
init() {
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
GameScene.swift
import SpriteKit
class GameScene: SKScene {
let defaults = UserDefaults.standard
var carArray = [Car]()
override func didMove(to view: SKView) {
for _ in 1...3 {
let car = Car()
car.engineSize = 2000
carArray.append(car)
}
// Save to defaults
defaults.set(NSKeyedArchiver.archivedData(withRootObject: carArray), forKey: "carArrayKey")
// Restore from defaults
let arrayData = defaults.data(forKey: "carArrayKey")
carArray = NSKeyedUnarchiver.unarchiveObject(with: arrayData!) as! [Car]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for c in carArray {
print("Car's engine size is \(c.engineSize)")
}
}
}
This answer led me to try implementing encoder/decoder methods on the Car
class:
Car.swift (updated)
import SpriteKit
class Car: SKSpriteNode {
var engineSize: Int = 0
init() {
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
}
required convenience public init(coder decoder: NSCoder) {
self.init()
if let engineSize = decoder.decodeObject(forKey: "engineSize") as? Int {
self.engineSize = engineSize
}
}
func encodeWithCoder(coder : NSCoder) {
coder.encode(self.engineSize, forKey: "engineSize")
}
}
However, this doesn't seem to be working. GameScene
is still printing the engineSize
as 0. Am I implementing the coder/decoder wrong? What can I do to prevent the engineSize
properties from resetting to 0 when they are restored from defaults?
UPDATE
Here is my updated Car
class as I'm trying to get it to work with rmaddy's suggestions:
import SpriteKit
class Car: SKSpriteNode {
var engineSize: Int = 0
init() {
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
if let engineSize = decoder.decodeObject(forKey: "engineSize") as? Int {
self.engineSize = engineSize
}
}
func encodeWithCoder(coder : NSCoder) {
super.encode(with: coder)
coder.encode(self.engineSize, forKey: "engineSize")
}
}
The code compiles and runs but the engineSize
property is still being reset to 0 when saving to defaults.
I managed to get this working. Here are the updated GameScene
and Car
code files with NSCoding
properly implemented.
Car.swift
import SpriteKit
class Car: SKSpriteNode {
var engineSize: Int = 0
init() {
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
}
required init(coder aDecoder: NSCoder) {
engineSize = aDecoder.decodeInteger(forKey: "engineSize")
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(engineSize, forKey: "engineSize")
}
}
GameScene.swift
import SpriteKit
class GameScene: SKScene {
let defaults = UserDefaults.standard
var carArray = [Car]()
override func didMove(to view: SKView) {
for _ in 1...3 {
let car = Car()
car.engineSize = 2000
carArray.append(car)
}
defaults.set(NSKeyedArchiver.archivedData(withRootObject: carArray), forKey: "carArrayKey")
let arrayData = defaults.data(forKey: "carArrayKey")
carArray = NSKeyedUnarchiver.unarchiveObject(with: arrayData!) as! [Car]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for c in carArray {
print("Car's engine size is \(c.engineSize)")
}
}
}
UPDATE
If you implement init(coder aDecoder: NSCoder)
and encode(with aCoder: NSCoder)
as shown above, you may notice that some type properties like color
, position
, or size
are being reset to their initial values after you archive/unarchive them. This is because you are now providing your own implementation of these two methods, so you can no longer rely on the superclass to handle the encoding/decoding of the type properties for you.
You must now implement the encoding/decoding for all properties that you care about, both your own custom properties and the type's properties. For example:
required init(coder aDecoder: NSCoder) {
super.init(texture: nil, color: .blue, size: CGSize(width: 100, height: 100))
engineSize = aDecoder.decodeInteger(forKey: "engineSize")
self.position = aDecoder.decodeCGPoint(forKey: "position")
self.alpha = aDecoder.decodeObject(forKey: "alpha") as! CGFloat
self.zPosition = aDecoder.decodeObject(forKey: "zPosition") as! CGFloat
self.name = aDecoder.decodeObject(forKey: "name") as! String?
self.colorBlendFactor = aDecoder.decodeObject(forKey: "colorBlendFactor") as! CGFloat
self.color = aDecoder.decodeObject(forKey: "color") as! UIColor
self.size = aDecoder.decodeCGSize(forKey: "size")
self.texture = aDecoder.decodeObject(forKey: "texture") as! SKTexture?
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(engineSize, forKey: "engineSize")
aCoder.encode(self.position, forKey: "position")
aCoder.encode(self.alpha, forKey: "alpha")
aCoder.encode(self.zPosition, forKey: "zPosition")
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.colorBlendFactor, forKey: "colorBlendFactor")
aCoder.encode(self.color, forKey: "color")
aCoder.encode(self.size, forKey: "size")
aCoder.encode(self.texture, forKey: "texture")
}