Passing Objects To WatchKit with NSKeyedArchiver - swift

class Parent: NSObject, NSCoding {
var name : String?
var childs : [Child]?
override init() {}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(childs, forKey: "childs")
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as? String
childs = aDecoder.decodeObject(forKey: "childs") as? [Child]
super.init()
}
}
class Child: NSObject, NSCoding {
var name: String?
override init() {}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as? String
super.init()
}
}
// iOS side:
public func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Swift.Void) {
var parent = Parent()
parent.name = "John"
var child1 = Child()
var child2 = Child()
parent.childs = [child1, child2]
NSKeyedArchiver.setClassName("Parent", for: Parent.self)
let data = NSKeyedArchiver.archivedData(withRootObject: parent)
replyHandler(["parent": data as AnyObject])
}
// watchOS side:
if WCSession.isSupported() {
session = WCSession.default()
session!.sendMessage(["getParent": "getParent"], replyHandler: { (response) -> Void in
if let rawParent = response as? [String:Data] {
NSKeyedUnarchiver.setClass(Parent.self, forClassName: "Parent")
if let parent = NSKeyedUnarchiver.unarchiveObject(with: data) as? Parent {
// do something
}
}
}, errorHandler: { (error) -> Void in
print(error)
})
}
in this case as you can see I used the NSKeyedUnarchiver.setClass and NSKeyedArchiver.setClassName methods which are working but I still get a runtime crash:
[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (Child) for key (NS.objects)
If I remove the NSKeyedUnarchiver.setClass(Parent.self, forClassName: "Parent") and the NSKeyedArchiver.setClassName("Parent", for: Parent.self) rows I get the same error but for Parent class. So I am sure that I have to use the same method call for Child but I dont know where.
Anybody has any idea?

Okey I found the solution, if you have the same structure you just simply set the class names after each other
NSKeyedArchiver.setClassName("Parent", for: Parent.self)
NSKeyedArchiver.setClassName("Child", for: Child.self)
and the same when you unarchive it:
NSKeyedUnarchiver.setClass(Parent.self, forClassName: "Parent")
NSKeyedUnarchiver.setClass(Child.self, forClassName: "Child")

Related

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.
This is the culprit since it returns nil:
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!

Getting optional String from singleton

I've created a class with some vars and lets. One of these vars is a String. I store them in UserDefaults. If I want to access the string of this class over a singleton class, I will always get an optional String. I don't know why.
Here is the class of the object:
import Foundation
import SpriteKit
class BallSkinsClass: NSObject, NSCoding {
let id: Int
var name: String
var isBuyed: Bool
let ID = "id"
let NAME = "name"
let ISBUYED = "isBuyed"
init(id: Int, name: String, isBuyed: Bool) {
self.id = id
self.name = name
self.isBuyed = isBuyed
}
required init?(coder aDecoder: NSCoder) {
self.id = aDecoder.decodeInteger(forKey: ID)
self.name = String(describing: aDecoder.decodeObject(forKey: NAME))
self.isBuyed = aDecoder.decodeBool(forKey: ISBUYED)
}
#objc func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: ID)
aCoder.encode(name, forKey: NAME)
aCoder.encode(isBuyed, forKey: ISBUYED)
}
}
To declare the skins, access, save and load I have these functions in my BallSkinsClass:
import Foundation
import SpriteKit
import GameKit
class BallSkins {
static var sharedInstance = BallSkins()
private init() {
}
let BALLSKINS = "ballSkins"
var standard: BallSkinsClass! = BallSkinsClass(id: 0, name: "Standard", isBuyed: true)
var billiard: BallSkinsClass! = BallSkinsClass(id: 1, name: "Billard", isBuyed: false)
var emoji: BallSkinsClass! = BallSkinsClass(id: 2, name: "Emojis", isBuyed: false)
func archiveBallSkins(ballSkins:[BallSkinsClass]) -> NSData {
print("archiving Skins")
let archivedBallSkins = NSKeyedArchiver.archivedData(withRootObject: ballSkins as Array)
return archivedBallSkins as NSData
}
func saveBallSkins(ballSkins:[BallSkinsClass]) {
let archivedBallSkins = archiveBallSkins(ballSkins: ballSkins)
UserDefaults.standard.set(archivedBallSkins, forKey: BALLSKINS)
print("saving Skins")
}
func retrieveBallSkins() -> [BallSkinsClass]? {
print("retrieving Skins")
if let unarchivedBallSkins = UserDefaults.standard.object(forKey: BALLSKINS) as? NSData {
return NSKeyedUnarchiver.unarchiveObject(with: unarchivedBallSkins as Data) as? [BallSkinsClass]
}
return nil
}
func loadBallSkins() {
print("loading Skins")
let archivedBallSkins = retrieveBallSkins()
for ballSkin in archivedBallSkins! {
switch ballSkin.id {
case 0 :
standard.isBuyed = ballSkin.isBuyed
case 1:
billiard.isBuyed = ballSkin.isBuyed
case 2:
emoji.isBuyed = ballSkin.isBuyed
default:
standard.isBuyed = ballSkin.isBuyed
}
}
}
}
If I want to access the name of the skin in any other scene or view I call:
ballSkins.sharedInstance.billiard.name
But this is an optional every time! I don't know why or where the error is.
I suppose it is caused by
required init?(coder aDecoder: NSCoder) {
self.id = aDecoder.decodeInteger(forKey: ID)
self.name = String(describing: aDecoder.decodeObject(forKey: NAME))
self.isBuyed = aDecoder.decodeBool(forKey: ISBUYED)
}
3rd line generates optional string because according to documentation
func decodeObject() -> Any?
and String(describing: ...) does not unwrap your value. You must unwrap all values from UserDefaults by yourself, providing defaultValue if nil is not possible

Custom Class Unarchive is nil in Cocoa Swift

I am trying to save and retrieve a custom class to UserDefaults in my macOS app. I am getting nil for newData
class countClass: NSObject, NSCoding {
var leftClickCount : Int = 0
init(leftClickCount: Int) {
self.leftClickCount = leftClickCount
super.init()
}
func encode(with coder: NSCoder) {
coder.encode(self.leftClickCount, forKey: "leftClickCount")
}
required convenience init?(coder decoder: NSCoder) {
guard let leftClickCount = decoder.decodeObject(forKey: "leftClickCount") as? Int
else {
return nil
}
self.init(
leftClickCount: leftClickCount
)
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let leftC = countClass(leftClickCount: 25)
let ud = UserDefaults.standard
let archivedData = NSKeyedArchiver.archivedData(withRootObject: leftC)
ud.set(archivedData, forKey: "data")
ud.synchronize()
let tempData = ud.object(forKey: "data") as! Data
let newData = NSKeyedUnarchiver.unarchiveObject(with: tempData) as! countClass // Getting nil here
}
}
I was able to fix this problem by changing from:
decoder.decodeObject(forKey: "leftClickCount") as? Int
with:
decoder.decodeInteger(forKey: "leftClickCount")

Swift3: [String: AnyObject] using NSCoding protocol to save with NSUserDefaults

I have my custom class:
private class PendingRequest : NSObject, NSCoding
{
var route : String!
var params: [String: AnyObject]!
init(route: String, params: [String: AnyObject])
{
self.route = route
self.params = params
}
required init(coder aDecoder: NSCoder)
{
self.route = aDecoder.decodeObject(forKey: "route") as! String
self.params = aDecoder.decodeObject(forKey: "params") as! [String: AnyObject]
}
public func encode(with coder: NSCoder)
{
coder.encode(route, forKey: "route")
coder.encode(params, forKey: "params")
}
}
And here is how I save my list of PendingRequest:
private static func savePendingRequests(requestsToSend: [PendingRequest])
{
let data = NSKeyedArchiver.archivedData(withRootObject: requestsToSend!)
UserDefaults.standard.set(data, forKey: self.requestsToSendDefaultsString)
}
And here is how I try to retrieve it:
let data = UserDefaults.standard.data(forKey: requestsToSendDefaultsString)
let requests = NSKeyedUnarchiver.unarchiveObject(with: data!) as! [PendingRequest]
return requests
But my code crashes when retrieving my data... Saving works fine, but not retrieving... Any idea?
EDIT: Here is my Crashlytics error:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_TtCC6Versus9VersusAPIP33_8D2B8AA415FDC4F8DFAF29D7ECE33C1F14PendingRequest) for key (NS.objects); the class may be defined in source code or a library that is not linked
SOLUTION:
My class PendingRequest was fileprivate, thus NSKeyedUnarchiver couldn't access it, hence the crash... Thanks for your help.
Few things are incorrect
you're forgetting to call super.init() in your initializers
init(route: String, params: [String: AnyObject])
{
self.route = route
self.params = params
super.init()
}
required init(coder aDecoder: NSCoder)
{
self.route = aDecoder.decodeObject(forKey: "route") as? String
self.params = aDecoder.decodeObject(forKey: "params") as? [String: AnyObject]
// Check if route and params are what you expected here
super.init()
}
You have a static method that calls "self", I'm not sure this works, you could still have a static key or a global key.
Plus you're unwrapping a non-optional value "requestsToSend".
I think this would solve it.
private static func savePendingRequests(requestsToSend:[PendingRequest])
{
let data = NSKeyedArchiver.archivedData(withRootObject: requestsToSend)
UserDefaults.standard.set(data, forKey: "AStaticKey")
}
Then retrieving should work. (I added a few safety checks)
guard let data = UserDefaults.standard.data(forKey: "AStaticKey"),
let requests = NSKeyedUnarchiver.unarchiveObject(with: data) as? [PendingRequest] else {
return [PendingRequest]()
}
return requests

Unarchiving a dictionary with unarchiveObjectWithFile using swift

I am facing problem with the data that I got from using unarchiveObjectWithFile. I send a dictionary for archiving. The data, I got shows the value while debugging but I can't extract the exact value for key from the data. Always getting nil value
Here is my code
class Person : NSObject, NSCoding {
struct Keys {
static let Name = "name"
static let Age = "age"
}
var name: [String] = []
var age: [Int] = []
init(dictionary: [String : AnyObject]) {
name = dictionary[Keys.Name] as! [String]
age = dictionary[Keys.Age] as! [Int]
}
func encodeWithCoder(archiver: NSCoder) {
archiver.encodeObject(name, forKey: Keys.Name)
archiver.encodeObject(age, forKey: Keys.Age)
}
required init(coder unarchiver: NSCoder) {
if let namesList = unarchiver.decodeObjectForKey(Keys.Name) as? [String] {
name = namesList
print("name\(name)")
}
if let agesList = unarchiver.decodeObjectForKey(Keys.Age) as? [Int] {
age = agesList
print("age \(age)")
}
super.init()
}
}
// View controller
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let persons: [String:AnyObject] = [
"name" : ["a","b","c"],
"age" : [2,3,4]
]
let personObj = Person(dictionary: persons)
NSKeyedArchiver.archiveRootObject(personObj, toFile: "/Users/shuvo/Desktop/Data.json")
let responseObject = NSKeyedUnarchiver.unarchiveObjectWithFile("/Users/shuvo/Desktop/Data.json") as? Person
if let unwrapData: AnyObject = responseObject {
let nameArr = unwrapData["name"] as? [String]
let ageArr = unwrapData["age"] as? [Int]
print("age \(ageArr) name \(nameArr)")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
That's because you are archiving the Person object and not the [String: AnyObject].
You can either archive the Person, and extract it as a Person or archive the [String: AnyObject] and extract that. You can't use both as they are different types.
If you want to archive the Person object, make sure you implement the NSCoding protocol and have something similar to this in your Person class...
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
}
required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as! [String]
self.age = aDecoder.decodeObject(forKey: "age") as! [Int]
}
After that, you can just do...
if let responseObject = NSKeyedUnarchiver.unarchiveObject(withFile: path) as? Person {
// You can use `responseObject` as a `Person` object here...
}