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.
Related
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.
noob swift guy here.
I have this UITextView where the user writes something. I need to store his "note" to the database with all the formated text in it. That means if it has bold, I need to display it as bold later.
This text should be stored in local Realm Swift Database. Any solutions how can I achieve that? I head about "Markdown", but I didn't understand how can that be useful.
Much thanks! Have a great day!
Realm only stores ascii text and cannot directly store any formatting properties like BOLD, and that's true for most databases.
If you want to store formatted strings, one option is to archive it to a Data type, which Realm fully supports. Then, when reading data back in, unarchive it from Data back to a formatted string. (formatted strings are often called Rich Text or in code: attributedStrings)
The below code is macOS but iOS will be similar
A quick example - here's a class to hold a some attributed string data
class MyAttrStringClass: Object {
#Persisted var attrStringData: Data!
}
Suppose we have a (rich) text field with the following text
Hello, World
To store the class with the attributed string in Realm:
let s = self.myRichTextField.attributedStringValue
let data = try! NSKeyedArchiver.archivedData(withRootObject: s, requiringSecureCoding: false)
let myObject = MyAttrStringClass()
myObject.attrStringData = data
try! realm.write {
realm.add(myObject)
}
then when reading back from Realm and displaying the attributed string in that same field
let myObject = realm.objects(MyAttrStringClass.self).first!
let data = myObject.attrStringData!
let s = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: data)
self.myRichTextField.attributedStringValue = s!
Note: there's no error checking in the above. Please protect your code by safely unwrapping optionals.
After a few months of learning Swift by migrating parts of a legacy ObjC app, I'm looking forward to starting a new app in pure Swift - I'm hoping that working with pure Swift base classes will lead to less unwrapping and other bridging shenanigans.
However, very quickly I've found myself facing similar problems.
I want to read some JSON from a web service, and show in in a list implemented with SwiftUI - should be simple, right?
The data (actually read from the Twitter API) comes in, and I deserialise it,
do {
if let results = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments ) as? [String: Any] {
print(results)
if let followers = results["users"] as? [[String: Any]] {
print (followers.count)
print("followers class \(type(of: followers))")
} else {
print(results["errors"])
}
}
} catch let error as NSError {
print(error.localizedDescription)
}
You'll see I print the class of followers, and this shows,
followers class Array<Dictionary<String, Any>>
...a nice array of Dictionaries, using Swift base classes. That would seem a reasonable data structure to present via a List in SwiftUI. However, it doesn't seem to be so simple, as the data elements need to be Identifiable (fair enough) but I don't have a struct for each element, and I don't want the overhead of processing the array into an array of structs carrying identifiers.
A bit of research, and it seems there's a solution available, as I can initialise List with an Array, something like the following,
var body: some View {
List (twitter.followers!, id: \.self) { follower in // <<< Compilation error
Text("\(follower["name"])")
}
}
However, that code gives the compilation error on the flagged line,
Protocol type 'Any' cannot conform to 'Hashable' because only concrete
types can conform to protocols
I think the issue is that the compiler sees followers as 'Any', rather than an Array, but why?
btw, I've seen the answer to this question, but it seems the List initialiser should be a more elegant solution, if I can get it to work...
You have to create a struct for follower/user and declare it as Decodable and Identifiable.
Then you will be able to use JSONDecoder to read the data from the struct into an array for you.
As a result your twitter.followers will be an array of identifiable objects and you will be able to use it in ForEach().
My app stores user's sensitive data and I wonder what is general/proper/the most secure way to secure the data.
My approach now is saving encoded data into Keychain, more specifically but simply :
I am encoding/decoding custom types in array with PropertyList
Then pass these to Keychain. I am using a wrapper called KeychainAccess thanks to Kishikawa Katsumi.
Retrieve data from Keychain then decode it to display on tableview.
Below is my code bits :
extension UserInformation {
static let propertyListEncoder = PropertyListEncoder()
static let propertyListDecoder = PropertyListDecoder()
static let keychain = Keychain(service: "com.xxxxx.DataSaving-App")
static func saveToKeychain(userInfo: [UserInformation]) {
let data = try? propertyListEncoder.encode(userInfo)
if let savingData = data {
dump(userInfo)
keychain[data: "encodedUserInfo"] = NSData(data: savingData) as Data
}
}
static func loadFromKeychain() -> [UserInformation]? {
guard let retrievedData = keychain[data: "encodedUserInfo"] else {
return nil }
let data = try? propertyListDecoder.decode(Array<UserInformation>.self, from: retrievedData)
return data
}
}
Above works fine and no problem found so far. However, I am not sure to use PropertyListEncoder is right way to encode/decode. Please kindly see below specified questions and giving advice to me should be very grateful.
Would above approach be secure enough?
Is PropertyListEncoder/Decoder just doing ENCODING/DECODING, NOT SAVING DATA TO ANY, like plist or any storage where is vulnerable?
If any other measure is needed, is encrypting data with Common Swift then passing it to Keychain making sense?
I spent few weeks to find detail solution but no luck yet. Please help a new developer.
Property list encoding is not encryption. It's plain text (possibly in binary format); it is just a form of serialization. The "encryption" in your example consists of the fact that the information is hidden in the keychain. And the keychain is generally regarded as sufficiently secure.
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.