Deserialize subclass of GKGraphNode using NSKeyedUnarchiver - swift

I want to serialize and deserialize an object of my GKGraphNode subclass using NSKeyedArchiver and NSKeyedUnarchiver. So I try the following:
//: Playground - noun: a place where people can play
import GameplayKit
class MyGraphNode: GKGraphNode {
static let textCodingKey = "TextCodingKey"
let text: String
override convenience init() {
self.init(text: "Default Text")
}
init(text: String) {
self.text = text
super.init()
}
required init?(coder aDecoder: NSCoder) {
text = aDecoder.decodeObject(forKey: MyGraphNode.textCodingKey) as! String
super.init(coder: aDecoder)
}
override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(text, forKey: MyGraphNode.textCodingKey)
}
}
let text = "Test Text"
let graphNode = MyGraphNode(text: text)
let data = NSKeyedArchiver.archivedData(withRootObject: graphNode)
if let unarchivedGraphNode = NSKeyedUnarchiver.unarchiveObject(with: data) as? MyGraphNode {
print("Text: \(unarchivedGraphNode.text)")
}
Unfortunately the example prints only the default text and not the expected test text:
Text: Default Text
First I omitted the convenience initializer. But in this case it crashed with this error:
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION
(code=EXC_I386_INVOP, subcode=0x0). The process has been left at the
point where it was interrupted, use "thread return -x" to return to
the state before expression evaluation.
GKGraphNodeSubclass.playground: 5: 7: Fatal error: Use of
unimplemented initializer 'init()' for class
'__lldb_expr_58.MyGraphNode'
Can anyone explain why the test text is ignored during the deserialization?
Or why I have to add the convenience initializer at all?

I got help in the Apple Developer Forum:
The "text" property is ending up being reset by "super.init(coder: aDecoder)", presumably because that calls "init()" internally, and that ends up at your convenience "init ()". This would be illegal in Swift, but it's legal in Obj-C, which doesn't have the same strict initialization rules.
The solution is to initialize "text" after the super.init(coder:), rather than before. This means you can't use a "let" property
To fix my example I changed the variable declaration and the NSCoding initializer like this:
var text: String!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
text = aDecoder.decodeObject(forKey: MyGraphNode.textCodingKey) as! String
}

I agree with Ruben, with a small addition
It might also be necessary to override the init() to ensure it exist. Due to the strict initialization rules of Swift
override init() {
super.init()
}

Related

Why is this subclass failing to be archived by NSKeyedArchived?

I have this class that have to be the subclass of OIDAuthState (this class is an NSObject and conforms to NSSecureCoding),
The code to encode the class OIDAuthState was working fine before
self.data = try NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: true)
Once I created this subclass that includes adds a simple Boolean to the class isExpiredTokenEnforced, I now get the error when using NSKeyedArchiver,
The error says:
NSDebugDescription=Caught exception during archival: *** -decodeBoolForKey: only defined for abstract class. Define -[NSKeyedArchiver decodeBoolForKey:]!
Here is the subclass I am trying to archive and down below the override method to encode and decode this class, I am simply encoding and decoding this 1 extra property and then passing down the coder to the parent class,
class AuthenticationStateManager: OIDAuthState, AuthenticationState {
var isExpiredTokenEnforced = false
var lastTokenResponseInterface: TokenResponse? {
if isExpiredTokenEnforced {
return EnforcedExpiredTokenResponse(idToken: super.lastTokenResponse?.idToken,
accessToken: super.lastTokenResponse?.accessToken)
} else {
return super.lastTokenResponse
}
}
override static var supportsSecureCoding: Bool { true }
required override init(authorizationResponse: OIDAuthorizationResponse?, tokenResponse: OIDTokenResponse?, registrationResponse: OIDRegistrationResponse?) {
super.init(authorizationResponse: authorizationResponse, tokenResponse: tokenResponse, registrationResponse: registrationResponse)
}
required init?(coder: NSCoder) {
coder.encode(isExpiredTokenEnforced, forKey: "isExpiredTokenEnforced")
super.init(coder: coder)
}
override func encode(with coder: NSCoder) {
self.isExpiredTokenEnforced = coder.decodeBool(forKey: "isExpiredTokenEnforced")
super.encode(with: coder)
}
}
I can't figure out why this is failing I couldn't find relevant information to fix this,
does anyone have a clue what could be wrong here?
Thank you in advance for your time.
You are mixing up your encoder and decoder functions. Your func encode() is trying to call decodeBool. It should be calling encode().
Similarly your init?(coder:) function should be calling decode (or rather decodeBool(forKey:).)

Xcode 12.5 NSCoding issue with SpriteKit/GameplayKit

I'm working on application using SpriteKit and I'm having trouble with NSCoding on a particular object (a GKComponent object). I'm using Swift 4.0 and iOS 11 in Build Settings. The application always crashes on the line (marked with //***) where I'm trying to decode the object. I've been using the same structure for encoding/decoding other objects and they seem to work fine. Not sure why it won't work with this particular GKComponent object.I've spent hours trying to resolve this issue but couldn't figure out why.
Any help will be appreciated.
Thanks in advance!
class ItemEntity: GKEntity {
var name: String!
var embeddedSpell: KnockbackComponent?
//Other variables
init(itemName: String) {
super.init()
name = itemName
embeddedSpell = KnockbackComponent(anItem: self)
addComponent(embeddedSpell!)
//Other initializations
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "Item.name")
aCoder.encode(embeddedSpell, forKey: "Item.embeddedSpell")
super.encode(with: aCoder)
}
required convenience init?(coder aDecoder: NSCoder) {
let itemName = aDecoder.decodeObject(forKey: "Item.name") as! String
self.init(itemName: itemName)
embeddedSpell = aDecoder.decodeObject(forKey: "Item.embeddedSpell") as? KnockbackComponent
if embeddedSpell != nil {
addComponent(self.embeddedSpell!)
}
}
//Other codes
}
Other class
class KnockbackComponent: GKComponent {
weak var item: ItemEntity!
init(anItem: ItemEntity) {
item = anItem
super.init()
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(item, forKey: "KnockbackComponent.item")
super.encode(with: aCoder)
}
required convenience init?(coder aDecoder: NSCoder) {
//***This line causes crash with error msg Thread 1: EXC_BAD_INSTRUCTION***//
let item = aDecoder.decodeObject(forKey: "KnockbackComponent.item") as? ItemEntity //<--error msg Thread 1: EXC_BAD_INSTRUCTION
self.init(anItem: item!)
}
//Other codes
}
I've resolved the issue by getting my GKComponent class to conform to Codable with the codes below:
override func encode(to encoder: Encoder) {
var container = encoder.container(keyedBy: CodingKeys.self)
try? (container.encode(itemEnt, forKey: .itemEnt))
}
required convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let itemEntity = try container.decode(ItemEntity.self, forKey: .itemEnt)
self.init(entity: itemEntity)
}

The required encoding function doesn't shown in a working code

serialization and deserialization is done by the two method defined in the NSCoding protocol as follow
encodeWithCoder(_ aCoder: NSCoder) {
// Serialize your object here
}
init(coder aDecoder: NSCoder) {
// Deserialize your object here
}
In my own ViewController (inherited from UITableViewController), I have a Array which contain my own object named Item and I do implemented the init(coder aDecoder: NSCoder) initial function. The Item is defined as follow
class Item: NSObject, NSCoding {
var text = ""
var checked = false
func toggleChecked() {
checked = !checked
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.text, forKey: "SLAText")
aCoder.encode(self.checked, forKey: "SLAChecked")
}
required init?(coder aDecoder: NSCoder) {
self.text = aDecoder.decodeObject(forKey: "SLAText") as! String
self.checked = aDecoder.decodeBool(forKey: "SLAChecked")
super.init()
}
override init() {
super.init()
}
}
Instead implement the function encode(with: NSCoder) I defined my own serialization function named saveItems()
func saveItems() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
archiver.encode(items, forKey: "ItemLists") //items is a array of type [Item]
archiver.finishEncoding()
data.write(to: dataFilePath(), atomically: true)
}
Question
Why the code is working with out implement the required NSCoding function? The code is simplified from a example of the book I'm studying, I didn't find the encodeWithCoder(_ aCoder: NSCoder) function at all. Isn't the required means you have to implemented it?
Thanks for your time and help
Why the code is working with out implement the required NSCoding function?
This is not true.
NSCoding is a protocol
First of all NSCoding is a protocol, not a function. And in order to conform a type to NSCoding you need to implement a method and an initializer
public func encode(with aCoder: NSCoder)
public init?(coder aDecoder: NSCoder)
Why does your code work?
Let's look at what you are doing
archiver.encode(items, forKey: "ItemLists")
Here items is defined as [Item] which is and array of Item(s).
And Array does conform to NSCoding. This can be easily tested by writing
let nums = [1, 2, 3]
if nums is NSCoding {
print("Array does conform to NSCoding")
} else {
print("Array does NOT conform to NSCoding")
}
The result is
Array does conform to NSCoding
Conclusion
Of course to work properly the element inside of the array must be conform to NSCodable too. And since the generic type of your Array is Item (which you made conform to NSCodable) the mechanism does work properly.

Archive and unarchive a custom class(have a property as object array),always failed

I am searching for a long time on net. But no use. Please help or try to give some ideas how to achieve this.
when unarchive app crashes and i get this message:
Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason:
'*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_TtCC11ArchiveTest8dogHouse3dog)
for key (NS.objects); the class may be defined in source code or a library that is not linked'
What does it means?
Anyone helps?Thanks in advance.
Now,I recognize the Terminating because other codes,like this:
enter image description here
if I try:
enter image description here
it's well done,buy why?code write in the viewDidLoad do anything else?which method linked the class?
I have a class:
let ModelPath = "Model.plist".cacheDir()
class dogHouse: NSObject , NSCoding{
var dogs:[Dog]?
required init(coder aDecoder: NSCoder)
{
dogs = aDecoder.decodeObject(forKey: "dogs") as? [Dog]
super.init()
}
func encode(with aCoder: NSCoder) {
if dogs != nil{
aCoder.encode(dogs, forKey: "dogs")
}
}
class Dog: NSObject , NSCoding {
var name : String?
required init(coder aDecoder: NSCoder)
{
name = aDecoder.decodeObject(forKey: "name") as! String?
super.init()
}
func encode(with aCoder: NSCoder) {
if name != nil{
aCoder.encode(name, forKey: "name")
}
}
//ArchiveModels
func saveModel() -> Bool{
return NSKeyedArchiver.archiveRootObject(self, toFile: ICModelPath)
}
//UnArchiveModels
class func loadArchiver() -> [Dog]?{
let obj = NSKeyedUnarchiver.unarchiveObject(withFile: ICModelPath)
if obj != nil {
print(obj.debugDescription)
let model = obj as? dogHouse
return model?.dogs
}
return nil
}
}
Maybe you missed a parameter in the init function?
required init(coder aDecoder: NSCoder)
{
dogs = aDecoder.decodeObject(forKey: "dogs") as? [Dog]
super.init(aDecoder) //HERE
}

How to code initwithcoder in Swift?

i'm newbie in swift adn i have problem with initwithcoder in swift.
I have class UserItem, i need it to save user login.
in objective c is like this
- (id)initWithCoder:(NSCoder *)decoder{
if (self = [super init]){
self.username = [decoder decodeObjectForKey:#"username"];
}
return self;
}
and in swift i'm trying like this
override init() {
super.init()
}
required init(coder decoder: NSCoder!) {
self.username = (decoder.decodeObjectForKey("username")?.stringValue)!
super.init(coder: decoder)
}
but if like above, i get error on code
super.init(coder: decoder)
error message is "extra argument 'coder' in call
i can't figure out anymore, so i'm try this code,
convenience init(decoder: NSCoder) {
self.init()
self.username = (decoder.decodeObjectForKey("username")?.stringValue)!
}
but, get error
.UserItem initWithCoder:]: unrecognized selector sent to instance 0x7fd4714ce010
what should i do? thanks before for your help.
I struggled with NSCoding (the protocol that you use to archive and unarchive objects) in the past and I'm seeing that you are going through the same pains. Hope this lessen it a bit:
class UserItem: NSObject, NSCoding {
var username: String
var anInt: Int
init(username: String, anInt: Int) {
self.username = username
self.anInt = anInt
}
required init?(coder aDecoder: NSCoder) {
// super.init(coder:) is optional, see notes below
self.username = aDecoder.decodeObjectForKey("username") as! String
self.anInt = aDecoder.decodeIntegerForKey("anInt")
}
func encodeWithCoder(aCoder: NSCoder) {
// super.encodeWithCoder(aCoder) is optional, see notes below
aCoder.encodeObject(self.username, forKey: "username")
aCoder.encodeInteger(self.anInt, forKey: "anInt")
}
// Provide some debug info
override var description: String {
get {
return ("\(self.username), \(self.anInt)")
}
}
}
// Original object
let a = UserItem(username: "michael", anInt: 42)
// Serialized data
let data = NSKeyedArchiver.archivedDataWithRootObject(a)
// Unarchived from data
let b = NSKeyedUnarchiver.unarchiveObjectWithData(data)!
print(a)
print(b)
The important thing is to match up the keys and the data types in encodeWithCoder(aCoder:) (the archive function) and init(coder:) (the unarchive function).
Where is confusing for beginners is what to do with the superclass. You should only include the superclass in the two functions above if the superclass itself conforms to NSCoding. NSObject does not provide that by itself. The idea is each class knows about its own properties, some of which are private. If the superclass cannot archive/unarchive, there's no need to call them.