Using NSKeyedUnarchiver to decode object to a new model class in Swift using Coddle - swift

I am working with an old Objective-C codebase that utilizes NSCoding, NSSecureCoding, and NSKeyedUnarchiver/NSKeyedArchiver to store a model in User Defaults. We are migrating to a new User Defaults layer and I am wondering if it possible to decode this object without having the underlying class. For instance, the current object being stored is UserModel. Is it possible for me to create a new class, NewUserModel, with the same properties then decode this object from User Defaults?
I have tried the following, see comments for results:
guard let userData: Data = UserDefaults.default.object(forKey: "user-data") else {
return nil
}
// This returns the object, but it is Any as we do not have the model class for this object
guard let restoredObject = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(userData) as? Any else {
return nil
}
let unarchiver: NSKeyedUnarchiver = NSKeyedUnarchiver(forReadingFrom: userData)
if let decoded = unarchiver.decodeTopLevelDecodable(NewUserModel.self, forKey: NSKeyedArchiveRootObjectKey) {
// this fails and does not decode the object even though the properties are identical
print(decoded)
}
// trying to access the properties individually also fails
if let userToken: String = unarchiver.decodeObject(forKey: "userToken") {
// fails
print(userToken)
}
// attempting to decode using JSONDecoder also fails as the data is not valid JSON
do {
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(NewUserModel.self, from: userData)
print(user)
} catch {
print(erro)
}
Basically I have the underlying data yet I need to decode this data manually to get the relevant information without having access to the exact class that was used to archive the data.

Is it possible for me to create a new class, NewUserModel, with the same properties then decode this object from User Defaults?
Yes.
See the -setClass:forClassName: method, which is described thus:
Sets a translation mapping on this unarchiver to decode objects encoded with a given class name as instances of a given class instead.
So you can use that method to tell your unarchiver to instantiate NewUserModel whenever it sees the class name UserModel in the archive.
There's also a class method +setClass:forClassName: that does the same thing for all unarchivers, which might be handy if you have to read the same data in multiple places.

Related

String as Member Name in Swift

I have an array of strings and a CoreData object with a bunch of variables stored in it; the strings represent each stored variable. I want to show the value of each of the variables in a list. However, I cannot find a way to fetch all variables from a coredata object, and so instead I'm trying to use the following code.
ListView: View{
//I call this view from another one and pass in the object.
let object: Object
//I have a bunch of strings for each variable, this is just a few of them
let strings = ["first_name", "_last_name", "middle_initial" ...]
var body: some View{
List{
ForEach(strings){ str in
//Want to pass in string here as property name
object.str
//This doesn't work because string cannot be directly passed in as property name - this is the essence of my question.
}
}
}
}
So as you can see, I just want to pass in the string name as a member name for the CoreData object. When I try the code above, I get the following errors: Value of type 'Object' has no member 'name' and Expected member name following '.'. Please tell me how to pass in the string as a property name.
CoreData is heavily based on KVC (Key-Value Coding) so you can use key paths which is much more reliable than string literals.
let paths : [KeyPath<Object,String>] = [\.first_name, \.last_name, \.middle_initial]
...
ForEach(paths, id: \.self){ path in
Text(object[keyPath: path]))
}
Swift is a strongly typed language, and iterating in a python/javascript like approach is less common and less recommended.
Having said that, to my best knowledge you have three ways to tackle this issue.
First, I'd suggest encoding the CoreData model into a dictionary [String: Any] or [String: String] - then you can keep the same approach you wanted - iterate over the property names array and get them as follow:
let dic = object.asDictionary()
ForEach(strings){ str in
//Want to pass in string here as property name
let propertyValue = dic[str]
//This doesn't work because string cannot be directly passed in as property name - this is the essence of my question.
}
Make sure to comply with Encodable and to have this extension
extension Encodable {
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NSError()
}
return dictionary
}
Second, you can hard coded the properties and if/else/switch over them in the loop
ForEach(strings){ str in
//Want to pass in string here as property name
switch str {
case "first_name":
// Do what is needed
}
}
Third, and last, You can read and use a technique called reflection, which is the closest thing to what you want to achieve
link1
link2

Swift - JSONSerialization invalid JSON

I'm accessing health records via HealthKit, the issue is when I inspecting the FHIR data, it isn't valid JSON data when checking using isValidJSONObject. I'm not too familiar with JSONSerialization, this is my first real use for it.
let jsonObject = try JSONSerialization.jsonObject(with: fhirRecord.data, options: [])
print(jsonObject)
{
lotNumber = 11111;
occurrenceDateTime = "2021-01-01”;
patient = {
reference = "resource:0";
};
performer = (
{
actor = {
display = “Some place here“;
};
}
);
resourceType = Immunization;
status = completed;
vaccineCode = {
coding = (
{
code = 1;
system = “URL_HERE”;
},
{
code = 28581000087106;
system = “URL_HERE”;
}
);
};
}
JSONSerialization has nothing to do with Codable, and generally should be avoided in Swift. It's only in Swift because it's bridged from ObjC, and has significant limitations even in ObjC.
isValidJSONObject doesn't tell you that JSON data is valid. It tells you that an ObjC object could be converted to JSON by JSONSerialization (again, completely unrelated to Codable).
Get rid of the JSONSerialization. Plug your JSON into https://app.quicktype.io to generate a Codable model for it, and use JSONDecoder to decode it. JSONSerialization will only give a [String: Any] which is extremely hard to work with in Swift (and not great in ObjC). JSONDecoder will give you a proper struct.
As #RobNapier pointed out I needed FHIRModels, from Apple. I'm able to get data more easily than using a messy Codable data approach manually.
import ModelsR4
let resource = try decoder.decode(Immunization.self, from: data)
print output is proper
28581000087106
28581000087106

Can Codable APIs be used to decode data encoded with NSKeyedArchiver.archivedData?

I'm converting a codebase from using NSCoding to using Codable. I have run into an issue when trying to restore data encoded with NSCoding. I have an object that was encoded with the code:
let encodedUser = NSKeyedArchiver.archivedData(withRootObject: user)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedUser, forKey: userKey)
userDefaults.synchronize()
It was previously being decoded with the code:
if let encodedUser = UserDefaults.standard.data(forKey: userKey) {
if let user = NSKeyedUnarchiver.unarchiveObject(with: encodedUser) as? User {
\\ do stuff
}
}
After updating the User object to be Codable, I attempted to update the archive/unarchive code to use PropertyListDecoder because I saw that archivedData(withRootObject:) returns data formatted as NSPropertyListBinaryFormat_v1_0. When running this code:
if let encodedUser = UserDefaults.standard.data(forKey: userKey) {
let user = try! PropertyListDecoder().decode(User.self, from: encodedUser)
\\ do stuff
}
I get a keyNotFound error for the first key I'm looking for, and breakpointing in my init(from: Decoder), I can see that the container has no keys. I also tried to use PropertyListSerialization.propertyList(from:options:format) to see if I could pass that result into PropertyListDecoder, but that function gave me an NSCFDictionary, which is structured in a really strange way but does appear to contain all the data I'm looking for.
So is it possible to decode an object using Codable APIs if it was encoded with NSKeyedArchiver.archivedData(withRootObject:)? How can I achieve this?
I decided that the answer is almost certainly no, you cannot use Codable to decode what NSCoding encoded. I wanted to get rid of all the NSCoding cruft in my codebase, and settled on a migration path where I have both protocols implemented in the critical data models to convert stored data from NSCoding to Codable formats, and I will remove NSCoding in the future when enough of my users have updated.

Append generic element to Realm List

I'm trying to set data in a Realm database in swift via Object schema properties. Add new objects has been straightforward (and also relate and object to another), but I can't fin the way to append objects to a List property.
Let's see the snippet of code I use to create objects and add to Realm database:
func save(elements: [String : Any], realmClassName: String, realm: Realm, listObject: Object) {
// Ge properties for Realm Object subclass with name realmClassName
let properties = realm.schema[realmClassName]
// Iterate through elements array
for element in elements {
// Instantiate Realm Object through its name (realmClassName)
let thisClass: AnyClass = NSClassFromString("\(namespace).\(name)")!
let realmClass = thisClass as! Object.Type
let object = realmClass.init()
// Iterate though Object Schema properties
for property in properties?.properties as [Property]! {
object[property.name] = element[property.name]
}
// Add Object to Realm database
realm.add(object)
// Here is where I want to append object to Object List
}
}
Question is, how to do something similar to:
listObject.append(object)
Some attempt trow error like:
Could not cast value of type 'RealmSwift.List<AppName.Cars>' (0x1117446d8) to 'RealmSwift.List<RealmSwift.Object>'
Thanks.

Catching exceptions when unarchiving using NSKeyedUnarchiver

We've got a Swift class which inherits from NSObject and implements NSCoding. We need to change the name of the class in code and in the archives on disk. Fortunately, we don't need to retain the data. Wiping the cached files and returning default data will be fine. The trouble is detecting when it fails and handling it appropriately.
Ok, to start with the data is written out like this (ignore liberties with force unwrapping etc. it's just to keep the question concise):
let someObject: [Int: MyClass] = ...
let data = NSKeyedArchiver.archivedData(withRootObject: someObject)
try! data.write(to: someUrl, options: .atomic)
This works fine. Our existing method of decoding is this (again, our code is safer in practice):
let someObject = NSKeyedUnarchiver.unarchiveObject(withFile: someUrl.path)! as! [Int: MyClass]
Now, when we rename our class, we are going to have to deal with the cases where it fails to decode. With NSKeyedUnarchiver, you can set a delegate which will have a method called if decoding fails. Before changing it, I wanted to re-write our existing decoder to make sure it can decode as is before I make any changes. Unfortunately, it can't. Here is what we've got:
let fileData = fileManager.contents(atPath: fileUrl.path)!
let unarchiver = NSKeyedUnarchiver(forReadingWith: fileData)
guard let myObject = try! unarchiver.decodeTopLevelObject() as? [Int: MyClass] else {
return [Int: MyClass]()
}
Now, I didn't expect any issues with this. However, the call to decodetopLevelObject() fails every time, simply returning nil (it's definitely not the cast that's the problem). I have no idea why. The documentation for this is effectively non-existent. There is a similar call which is decodeObject(), but that also fails for no obvious reason. I've tried setting the delegate and implementing the method, but it never gets called.
Where are we going wrong?
Try using decodeObject(forKey:) with NSKeyedArchiveRootObjectKey or similar API.