Search code examples
iosswiftsprite-kitnskeyedarchiveruserdefaults

Properties reset when using NSKeyedArchiver on UserDefaults


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.


Solution

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