Use Codable to serialize custom class containing reference loop - swift

I am trying to serialize a custom class containing a reference loop and got it working using NSCoding:
import Foundation
class Person: NSObject, NSCoding {
let name: String
weak var parent: Person?
var child: Person?
init(name: String) {
self.name = name
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
self.parent = aDecoder.decodeObject(forKey: "parent") as? Person
self.child = aDecoder.decodeObject(forKey: "child") as? Person
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(parent, forKey: "parent")
aCoder.encode(child, forKey: "child")
}
}
let per = Person(name: "Per")
let linda = Person(name: "Linda")
linda.child = per
per.parent = linda
var people = [Person]()
people.append(linda)
people.append(per)
let encodedData = NSKeyedArchiver.archivedData(withRootObject: people)
let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: encodedData) as! [Person]
myPeopleList.forEach({
print("\($0.name)\n\t Child: \($0.child?.name ?? "nil")\n\t Parent: \($0.parent?.name ?? "nil")"
)})
// Linda
// Child: Per
// Parent: nil
// Per
// Child: nil
// Parent: Linda
No I want to do the same using Codable:
import Foundation
class Person: Codable {
let name: String
weak var parent: Person?
var child: Person?
init(name: String) {
self.name = name
}
}
let per = Person(name: "Per")
let linda = Person(name: "Linda")
linda.child = per
per.parent = linda
var people = [Person]()
people.append(linda)
people.append(per)
let archiver = NSKeyedArchiver()
try archiver.encodeEncodable(people, forKey: NSKeyedArchiveRootObjectKey)
But I get the error:
error: Execution was interrupted, reason: EXC_BAD_ACCESS
During the last line. I assume it has to do with the reference loop, because it works if I comment out the line:
per.parent = linda
So can we use Codable to serialize reference loops? If so, how?

You can choose which properties are serialized by overriding Coding Keys (from here)
e.g. in your case within the class:
enum CodingKeys: String, CodingKey
{
case name
case child
}
Only the keys included here should be saved, so no child->parent loop. This does however mean the connection will only exist in one direction when loading, so you will have to re-connect them when loaded.
FWIW, if you're dealing with 2-way relationships you will be much better off using a database rather than using NSKeyedArchiver for persistence.

Related

How to unarchive object using NSKeyedUnarchiver?

**I'm using this class: **
class Person: NSObject, NSCoding {
var name: String
var image: String
init(name: String, image: String) {
self.name = name
self.image = image
}
required init(coder aDecoder: NSCoder){
name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
image = aDecoder.decodeObject(forKey: "image") as? String ?? ""
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(image, forKey: "image")
}
}
For archiving, i used this method:
if let savedData = try? NSKeyedArchiver.archivedData(withRootObject: people, requireSecureCoding: false)
whrere people is Person class array [Person]
As for unarchiving, this method:
NSKeyedUnarchiver.unarchivedTopLevelObjectWithData()
is deprecated... which method should i use now ?
You now must tell the system what type you expect rather than simply unarchiving whatever is found:
try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: Person.self, from: savedData)

How to save a CapturedRoom using NSCoder

I'm trying to build an app that creates a floor plan of a room. I used ARWorldMap with ARPlaneAnchors for this but I recently discovered the Beta version of the RoomPlan API, which seems to lead to far better results.
However, I used te be able to just save an ARWorldMap using the NSCoding protocol, but this throws an error when I try to encode a CapturedRoom object:
-[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x141c18110
My code for encoding the class containing the CapturedRoom:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var capturedRoom: CapturedRoom
var title: String
var notes: String
init(capturedRoom: CapturedRoom, title: String, notes: String) {
self.capturedRoom = capturedRoom
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "capturedRoom") as? CapturedRoom,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
capturedRoom: capturedRoom,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(capturedRoom, forKey: "capturedRoom")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
To be clear, the following code does work:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var worldMap: ARWorldMap
var title: String
var notes: String
init(worldMap: ARWorldMap, title: String, notes: String) {
self.worldMap = worldMap
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "worldMap") as? ARWorldMap,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
worldMap: worldMap,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(worldMap, forKey: "worldMap")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
I'm writing the object to a local file using NSKeyedArchiver so it would be nice if I could keep the same structure using NSCoder. How can I fix this and save a CapturedRoom?
The issue is about saving CaptureRoom. According to the doc, it's not NS(Secure)Coding compliant, but it conforms to Decodable, Encodable, and Sendable
So you can use an Encoder/Decoder, to do CaptureRoom <-> Data, you could use the bridge NSData/Data, since NSData is NS(Secure)Coding compliant.
So, it could be something like the following code. I'll use JSONEncoder/JSONDecoder as partial Encoder/Decoder because they are quite common.
Encoding:
let capturedRoomData = try! JSONEncoder().encode(capturedRoom) as NSData
coder.encode(capturedRoomData, forKey: "capturedRoom")
Decoding:
let captureRoomData = coder.decodeObject(forKey: "capturedRoom") as! Data
let captureRoom = try! JSONDecoder().decode(CaptureRoom.self, data: captureRoomData)
Side note:
I used force unwrap (use of !) to simplify the code logic, but of course, you can use do/try/catch, guard let, if let, etc.)

how to access an NSObject variable from a structure in swift

I have structure that consists of an a few variables.
struct Contact {
var id:String = "Contact - \(UUID())"
var fullname: String
var exercises : [Exercise]
}
The part i am interest in is the exercises section. This variable takes the following class:
class Exercise : NSObject , NSSecureCoding{
static var supportsSecureCoding: Bool = true
var excerciseName: String
var excerciseReps: String
var excerciseSets: String
var excerciseWeights: String
init(Name : String, Reps : String, Sets : String, Weights : String) {
excerciseName = Name
excerciseReps = Reps
excerciseSets = Sets
excerciseWeights = Weights
}
func encode(with aCoder: NSCoder) {
aCoder.encode(excerciseName, forKey: "excerciseName")
aCoder.encode(excerciseReps, forKey: "excerciseReps")
aCoder.encode(excerciseSets, forKey: "excerciseSets")
aCoder.encode(excerciseWeights, forKey: "excerciseWeights")
}
required convenience init?(coder aDecoder: NSCoder) {
let excerciseName = aDecoder.decodeObject(forKey: "excerciseName") as! String
let excerciseReps = aDecoder.decodeObject(forKey: "excerciseReps") as! String
let excerciseSets = aDecoder.decodeObject(forKey: "excerciseSets") as! String
let excerciseWeights = aDecoder.decodeObject(forKey: "excerciseWeights") as! String
self.init(Name: excerciseName, Reps: excerciseReps, Sets: excerciseSets, Weights: excerciseWeights)
}
}
In the view controller i want to access these variables in i have activated it:
var contacts = [Contact]()
My problem is when i am trying to access it it doesn't give me the option. when i type self.contacts. then nothing appears. i was expecting self.contacts.excercises to be there on auto fill. it that option isn't there. what am i missing?

RLMArray in swift with decoder : Ambiguous reference to member error

I want to use Realm in mixed Objective-C & Swift app working with Codable and Realm Object can be export to Objective-C ;
class Person2 : RLMObject,Decodable {
#objc dynamic var name = ""
convenience init(_ name:String) {
self.init()
self.name = name
}
}
class RepairShop2 : RLMObject,Decodable {
#objc dynamic var name = ""
#objc dynamic var contact:Person2?
#objc dynamic var persons = RLMArray<Person2>(objectClassName: Person2.className())
private enum RepairShop2CodingKeys: String, CodingKey {
case name
case contact
case persons
}
convenience init(name: String, contact: Person2, persons: RLMArray<Person2>) {
self.init()
self.name = name
self.contact = contact
self.persons = persons
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RepairShop2CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let contact = try container.decode(Person2.self, forKey: .contact)
let personArray = try container.decode(RLMArray<AnyObject>, forKey: .persons)
// this line error: Ambiguous reference to member 'decode(_:forKey:)'**
let persons = RLMArray<Person2>(objectClassName: Person2.className())
persons.addObjects(personArray)
self.init(name: name, contact: contact, persons: persons)
}
}
let personArray = try container.decode(RLMArray<AnyObject>, forKey: .persons)
// this line error: Ambiguous reference to member 'decode(_:forKey:)'**
RLMArray.self I also tried , fail
how to write decode type of RLMArray?
RLMRealm doesn't conform to Decodable so you'll not be able to parse it into RLMRealm straight away. Instead try something like:
let persons = RLMArray<Person2>(objectClassName: Person2.className())
persons.addObjects(try container.decode([Person2].self, forKey: .persons) as NSFastEnumeration)
As a side note. Mixing different domains into one model is bad idea, it may bite later.

How to set a class instance to userdefaults in swift?

I researched it, but I couldnt find the right solution. I want to set a class instance to userDefaults. Assume that I have a class like this:
class Person {
var id: Int
var name: String
var email: String
init() {
self.id = 0
self.name = ""
self.email = ""
}
}
I created an instance from person class, after WebService call finished and I did:
var person: Person = Person()
person.id = personJSON.valueForKey(WSConstants.USER_ID) as Int
person.name = personJSON.valueForKey(WSConstants.USER_NAME) as String
person.email = personJSON.valueForKey(WSConstants.USER_EMAIL) as String
and then, I actually want to do this:
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setObject(person, forKey: "personInfo")
userDefaults.synchronize()
but I it is wrong, what is the right way to set a class instance to userDefaults ?
Thanks for your answers,
Best regards
As per NSUserDefaults Class Reference:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
You need to serialize your Person instance into one of those objects before saving it into the user defaults.
One of many ways to do it is to implement NSCoding protocol in your class:
class Person: NSCoding {
required init(coder aDecoder: NSCoder) {
self.id = aDecoder.decodeIntegerForKey("id")
self.name = aDecoder.decodeStringForKey("name")
self.email = aDecoder.decodeStringForKey("email")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeInteger(self.id, forKey: "id")
aCoder.encodeString(self.name, forKey: "name")
aCoder.encodeString(self.email, forKey: "email")
}
}
// when writing your defaults...
let personData = NSKeyedArchiver.archivedDataWithRootObject(person)
userDefaults.setObject(personData, forKey: "personInfo")
// then, when reading your defaults...
let personData = userDefaults.objectForKey("personInfo") as NSData?
if let personData = personData? {
let person = NSKeyedUnarchiver.unarchiveObjectWithData(personData) as Person
}
Alternatively, you can just save your personJSON dictionary to the defaults.