0

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.

peacetype
  • 1,928
  • 3
  • 29
  • 49
  • You need to call `super` in both the `init(coder:)` and `encodeWithCoder(coder:)` methods of `Car`. – rmaddy Jan 11 '18 at 03:51
  • 1
    You should also delete and reinstall your app to ensure there's no bad data in UserDefaults from earlier attempts. – rmaddy Jan 11 '18 at 03:52
  • I added `super.encode(with: coder)` okay, but in `init(coder:)` the compiler didn't like a call to `super.init(decoder)`. It complained: `Convenience initializer for 'Car' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')` – peacetype Jan 11 '18 at 05:12
  • `init(coder:)` should not be marked as a convenience initializer. – rmaddy Jan 11 '18 at 05:14
  • @rmaddy Removing `convenience` produces an error. I updated the post to show the code I'm trying. – peacetype Jan 11 '18 at 05:53
  • Right, you need to call `super.init(coder: decoder)`, not `self.init()`. – rmaddy Jan 11 '18 at 05:55
  • @rmaddy Code updated. It now runs successfully but the class property is still being reset to 0. – peacetype Jan 11 '18 at 06:07
  • Use the debugger. Confirm the expected methods are being called when you archive and unarchive. – rmaddy Jan 11 '18 at 06:09
  • Ok, thanks for your help. I'll play with it some more and study up on `NSCoding`. I haven't used that before and am pretty clueless as to how it works. – peacetype Jan 11 '18 at 06:12

1 Answers1

-1

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")
}
peacetype
  • 1,928
  • 3
  • 29
  • 49