Encoding and decoding SKSpriteNode

I am a little bit confused about how to decode an SKSpriteNode.


I have the following SKSpriteNode class:


class Person : SKSpriteNode
    {
      var gender : Gender
      var age : Age
      var positionX : CGFloat!
      var positionY : CGFloat!
      let frontTexture : SKTexture
      let backTexture : SKTexture


      required convenience init?(coder aDecoder: NSCoder)
      {
        print("INITIALIZED WITH DECODER????")
       
        guard let myPerson = aDecoder.decodeObject(forKey: "person") as? Person
            else { return nil }
       
        self.init(coder: aDecoder)
      }

      func encodeWithCoder(coder: NSCoder)
      {
        coder.encode(self, forKey: "person")
      }

      init(gender: Gender, age: Age, positionX: CGFloat, positionY: CGFloat, sceneSize: CGSize)
      {
        self.gender = gender
        self.age = age
       
        self.backTexture = SKTexture(imageNamed: "person_back")
        self.frontTexture = SKTexture(imageNamed: "person_front")

        var currentTexture = self.frontTexture
       
        super.init(texture: currentTexture, color: .clear, size: frontTexture.size())
       
        // Position of the person in the scene
        self.position = updatePosition(positionX: positionX, positionY: positionY, sceneSize: sceneSize)
      }


In a MultipeerConnection subclass, I use this to send encoded data (data seems to be archived correctly):


func sendData(dictionaryWithData dictionary: Dictionary<string, any="">, toPeer targetPeers: [MCPeerID])
      {
        let dataToSend = NSKeyedArchiver.archivedData(withRootObject: dictionary)
       
        do
        {
            try session.send(dataToSend, toPeers: targetPeers, with: MCSessionSendDataMode.reliable)
        }
        catch
        {
            print("ERROR")
        }
      }


with data made as follows (crowd is an array [Person]):


func makeMessageCrowd() -> Dictionary<string, any="">
      {
        var messageToSend = Dictionary<string, any="">()
        messageToSend["move"] = "start"
        messageToSend["action"] = appDelegate.persons
        return messageToSend
      }


I use the following to receive the data:


func session(_ session: MCSession,
                 didReceive data: Data,
                 fromPeer peerID: MCPeerID)
      {
       
        let message = NSKeyedUnarchiver.unarchiveObject(with: data) as! Dictionary<string, any="">
       
        if let moveType = message["move"] as? String
        {
            switch(moveType)
            {
            case "start":
                appDelegate.persons = message["action"] as! [Person]
               
            default:
                print("default")
            }
        }
      }


Right now, it starts initializing object Person with the convenience initializer, and then crashes here:


let message = NSKeyedUnarchiver.unarchiveObject(with: data) as! Dictionary<string, any="">


saying that it found a nil value. However, data seems to have 15000 bytes, so it was passed well.


If instead of a convenience initializer, I use this:


required init?(coder aDecoder: NSCoder)
      {
        //fatalError("NSCoding not supported")
        self.gender = Gender.male
        self.age = Age.young
        self.frontTexture = SKTexture(imageNamed: "person_front")
        self.backTexture = SKTexture(imageNamed: "person_back")
        self.positionX = 0
        self.positionY = 0

        super.init(coder: aDecoder)
      }


I do not have a crash, however the array [Person] on the peer device is created using the default values initialized as above, and not as passed.


The culprit is definitely this as it returns nil in the first case:

guard let myPerson = aDecoder.decodeObject(forKey: "person") as? Person
            else { return nil }


How do I do encode and decode a custom class properly?


Thanks a lot!

Accepted Reply

Seems you are misunderstanding how NSCoding works.


Unless you implement NSCoding methods properly, `decodeObject(forKey:)` can never decode an instance of `Person` class, and `encode(_:forKey:)` can never encode an instance of `Person` class correctly.


Which means, you cannot use `encode(_:forKey)` with an instance of `Person` to implement `encode(with:)` (not `encodeWithCoder(coder:)`) of Person,

and you cannot use `decodeObject(forKey:)` to decode an instance of `Person` inside the initializer `init(coder:)` of `Person`.


I cannot show you a concrete example as you are not showing the definition of `Gender` or `Age`, but your `Person` would be something like this, when your want it to comform to `NSCoding` properly:

class Person : SKSpriteNode {
    var gender : Gender
    var age : Age
    var positionX : CGFloat //<- Do not use implicitly unwrapped Optional
    var positionY : CGFloat //<- Do not use implicitly unwrapped Optional
    let frontTexture : SKTexture
    let backTexture : SKTexture
    
    required init?(coder aDecoder: NSCoder) {
        print("INITIALIZED WITH DECODER????")
        self.gender = Gender(rawValue: aDecoder.decodeInteger(forKey: "gender"))! //This may not work
        self.age = Age(rawValue: aDecoder.decodeInteger(forKey: "age"))! //This may not work
        self.positionX = CGFloat(aDecoder.decodeDouble(forKey: "positionX"))
        self.positionY = CGFloat(aDecoder.decodeDouble(forKey: "positionY"))
        self.frontTexture = aDecoder.decodeObject(forKey: "frontTexture") as! SKTexture
        self.backTexture = aDecoder.decodeObject(forKey: "backTexture") as! SKTexture
        super.init(coder: aDecoder) //<- you need to use `super`, `self.init(coder:)` just calls the initializer of `Person` recursively
    }
    
    override func encode(with aCoder: NSCoder) { //<- not `encodeWithCoder(coder: NSCoder)`
        super.encode(with: aCoder)
        aCoder.encode(gender.rawValue, forKey: "gender") //This may not work
        aCoder.encode(age.rawValue, forKey: "age") //This may not work
        aCoder.encode(Double(positionX), forKey: "positionX")
        aCoder.encode(Double(positionY), forKey: "positionY")
        aCoder.encode(frontTexture, forKey: "frontTexture")
        aCoder.encode(backTexture, forKey: "backTexture")
  
    }

    // You may need to fix other parts of your code...

}

Replies

Seems you are misunderstanding how NSCoding works.


Unless you implement NSCoding methods properly, `decodeObject(forKey:)` can never decode an instance of `Person` class, and `encode(_:forKey:)` can never encode an instance of `Person` class correctly.


Which means, you cannot use `encode(_:forKey)` with an instance of `Person` to implement `encode(with:)` (not `encodeWithCoder(coder:)`) of Person,

and you cannot use `decodeObject(forKey:)` to decode an instance of `Person` inside the initializer `init(coder:)` of `Person`.


I cannot show you a concrete example as you are not showing the definition of `Gender` or `Age`, but your `Person` would be something like this, when your want it to comform to `NSCoding` properly:

class Person : SKSpriteNode {
    var gender : Gender
    var age : Age
    var positionX : CGFloat //<- Do not use implicitly unwrapped Optional
    var positionY : CGFloat //<- Do not use implicitly unwrapped Optional
    let frontTexture : SKTexture
    let backTexture : SKTexture
    
    required init?(coder aDecoder: NSCoder) {
        print("INITIALIZED WITH DECODER????")
        self.gender = Gender(rawValue: aDecoder.decodeInteger(forKey: "gender"))! //This may not work
        self.age = Age(rawValue: aDecoder.decodeInteger(forKey: "age"))! //This may not work
        self.positionX = CGFloat(aDecoder.decodeDouble(forKey: "positionX"))
        self.positionY = CGFloat(aDecoder.decodeDouble(forKey: "positionY"))
        self.frontTexture = aDecoder.decodeObject(forKey: "frontTexture") as! SKTexture
        self.backTexture = aDecoder.decodeObject(forKey: "backTexture") as! SKTexture
        super.init(coder: aDecoder) //<- you need to use `super`, `self.init(coder:)` just calls the initializer of `Person` recursively
    }
    
    override func encode(with aCoder: NSCoder) { //<- not `encodeWithCoder(coder: NSCoder)`
        super.encode(with: aCoder)
        aCoder.encode(gender.rawValue, forKey: "gender") //This may not work
        aCoder.encode(age.rawValue, forKey: "age") //This may not work
        aCoder.encode(Double(positionX), forKey: "positionX")
        aCoder.encode(Double(positionY), forKey: "positionY")
        aCoder.encode(frontTexture, forKey: "frontTexture")
        aCoder.encode(backTexture, forKey: "backTexture")
  
    }

    // You may need to fix other parts of your code...

}

Thank you much! Extremely helpful. I do understand now how it works.


Cheers.