Save a Codable to UserDefaults and matching the look of NSCoding - ios

I have a struct called Station that supports the Codable protocol. Since it is a struct, it cannot support the NSCoding protocol. I would like to save an array of these Stations to the UserDefaults. I have code that does it without any problems.
func updateRecentStations(stations: [Station]) {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let data = try! encoder.encode(stationsArray)
UserDefaults.standard.set(data, forKey: "RecentStations")
}
func readRecentStations() -> [Station] {
if let data = UserDefaults.standard.object(forKey: "RecentStations") as? Data {
let stations: [Station] = try! PropertyListDecoder().decode( [Station].self, from: data)
return stations
}
}
These both work. The roundtrip is happening without a problem. But if I examine the <<app>>.plist in Xcode, or call print(Array(UserDefaults.standard.dictionaryRepresentation())) the displayed data is illegible. The Data that has been written looks like it is stored in a hexadecimal format. That said, it I open the <<app>>.plist within a text editor, I can see the xml within there, but the .plist isn't meant to be read that way.
The UserDefaults does not appear to have support for writing a plist to the .plist. Everything works, but because the plist editor can't read what I've written, I feel I am not doing things in the proper way.
Is there a way to support a Codable being written to the UserDefaults in such a way that it maintains its structure in a human readable way within there? To be clear, I am looking for a way that replicates the way NSCoding would add to the NSUserDefaults hierarchy.

You are storing Data in UserDefaults. That will get encoded as a base64 string which is why it isn't human readable.
Since you are encoding to an XML format, the resulting Data will actually be the data of a string representing the XML. So create a String from data. Then store that string in UserDefaults. Then the XML will be readable when viewing the UserDefaults plist file.
Note: the following hasn't been tested. There may be typos.
func updateRecentStations(stations: [Station]) {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let data = try! encoder.encode(stationsArray)
let xml = String(data: data, encoding: .utf8)!
UserDefaults.standard.set(xml, forKey: "RecentStations")
}
func readRecentStations() -> [Station] {
if let xml = UserDefaults.standard.object(forKey: "RecentStations") as? String {
let data = xml.data(using: .utf8)!
let stations: [Station] = try! PropertyListDecoder().decode( [Station].self, from: data)
return stations
}
}

Related

Where to store user data persistently across app updates (iOS)?

I am writing an app that contains a local database. I would like to give the user the possibility to bookmark some items in this database, and that these bookmarks do not disappear every time the app (and thus the database) is updated. What is the best solution in this case?
Simplest method of persisting data across app restarts is by using UserDefaults. In your case you can store custom data types in a UserDefaults key as follows:
Init defaults object
let defaults = UserDefaults.standard
Create a custom data type for your data (needs to be Codable)
struct Bookmark: Codable {
let name: String
let url: String
}
struct Bookmarks: Codable {
let bookmarks: [Bookmark]
}
Save data as follows
// data is of type Bookmarks here
if let encodedData = try? JSONEncoder().encode(data) {
defaults.set(encodedData, forKey: "bookmarks")
}
Retrieve data later
if let savedData = defaults.object(forKey: "bookmarks") as? Data {
if let savedBookmarks = try? JSONDecoder().decode(Bookmarks.self, from: savedData) {
print("Saved user: \(savedBookmarks)")
}
}
More info here and here

How to save a dictionary (or any other complex structure) into iCloud using Cloudkit?

I need to bind a date to a value and save it into Cloudkit. Rather than just save a new CKRecord for every object and subscript "date" and "value", I would prefer that the CKRecord is an array of dictionary: [(date, value)]. I've looked around and can't find any examples of this type of Cloudkit data storage. I would have thought by now that Codable would have been bridged over to Cloudkit but I don't see anything indicating that. There is a library that handles this but it doesn't handle this type of nesting. Is there anyway to do this?
Although CKRecord does have the ability to support an Array, it is primarily for storing simple data types like strings, numbers, and CKRecord.Reference. You don't specifically call out what your 'value' type is, but here is an example of using JSONEncoder/JSONDecoder to add support for writing/reading any codable type to a CKRecord. The encoder/decoder is simply converting the Encodable/Decodable type to/from a binary Data representation, which CKRecord also supports.
private let encoder: JSONEncoder = .init()
private let decoder: JSONDecoder = .init()
extension CKRecord {
func decode<T>(forKey key: FieldKey) throws -> T where T: Decodable {
guard let data = self[key] as? Data else {
throw CocoaError(.coderValueNotFound)
}
return try decoder.decode(T.self, from: data)
}
func encode<T>(_ encodable: T, forKey key: FieldKey) throws where T: Encodable {
self[key] = try encoder.encode(collection)
}
}
Usage would look like the following:
let collection: [[Date: String]] = [[:]]
let record = CKRecord(recordType: "MyRecord")
try? record.encode(collection, forKey: "collection")
let persisted = try? record.decode(forKey: "collection") as [[Date: String]]

UserDefaults not working with MSMessagesAppViewController

I am trying to save data to UserDefaults using an UIAlertController , but the code fails to retrieve the data.
Retrieve the Data
func displayCreatedPhrases(){
phrases = uDefaults.stringArray(forKey: "createdPhrases") ?? [String]()
print(phrases)
}
Setting the Data
self.uDefaults.set(textfield.text, forKey: "createdPhrases")
self.uDefaults.synchronize()
print("Saved the data!")
self.phraseTableView.reloadData()
You are setting the string as a value for key 'createdPhrases' and then asking uDefaults to return an array of string?
func displayCreatedPhrases() {
phrases = uDefaults.value(forKey: "createdPhrases") as? String
print(phrases)
}
The above code should work for you.
Also no need to use below line (Link to UserDefaults synchronize)
self.uDefaults.synchronize()
Reason:
textfield.text is of type String? and not [String]. In your code, you're saving the data as a String value and retrieving it as an [String] value. That's the reason, retrieving the data doesn't work.
Solution:
func displayCreatedPhrases(){
phrases = uDefaults.string(forKey: "createdPhrases") ?? ""
print(phrases)
}
Also, phrases must be of type String.

How to put datas of a plist includes rows of dictionaries to an array of dictionaries to use it on codes

I am working on a swift project and I need to read a long plist includes string data and then put it on an array of strings to use it on codes. and I did it and it works fine. I used these codes to reach it
if let URL = Bundle.main.url(forResource: "Abbrivation", withExtension: "plist") {
if let englishFromPlist = NSArray(contentsOf: URL) as? [String] {
myPlistArray = englishFromPlist
}
}
print("count of plist is:",myPlistArray.count)
now my project has changed a little and I have to work with a plist includes rows of dictionaries like the image below
now I have to change all the strings definition for array to dictionary but my problem is that all changes confronts an error. and I searched a lot how to do it but it failed.
now my question is that how to put data of a plist includes some rows of dictionaries to an array of dictionaries. I will appreciate any kinds of help. thank you
You are highly encouraged to decode the property list into a struct with the Codable protocol
struct Country : Decodable {
let flagEmoji, abb, countryName, pName : String
private enum CointryKeys : String, CodingKey { case flagEmoji, abb = "Abb", countryName, pName }
}
var countries = [Country]()
let url = Bundle.main.url(forResource: "Abbrivation", withExtension: "plist")!
let data = try! Data(contentsOf: url)
do {
countries = try PropertyListDecoder().decode([Country].self, from data}
} catch { print(error) }
print("count of plist is:", countries.count)
The code must not crash. If it does you made a design mistake
Note:
Never use the API NSArray(contentsOf in Swift to read a property list file from disk. Use always PropertyListSerialization or PropertyListDecoder

NSKeyedArchiver does not archive my Codable class

Here is my Codable class:
class SensorOutput: Codable {
var timeStamp: Date?
var gyroX: Double?
var gyroY: Double?
var gyroZ: Double?
var accX: Double?
var accY: Double?
var accZ: Double?
var magX: Double?
var magY: Double?
var magZ: Double?
init() {}
}
Here I try to write and read the object of that class to file:
let myData = SensorOutput()
myData.timeStamp = Date()
myData.gyroX = 0.0
myData.gyroY = 0.0
myData.gyroZ = 0.0
myData.accX = 0.0
myData.accY = 0.0
myData.accZ = 0.0
myData.magX = 0.0
myData.magY = 0.0
myData.magZ = 0.0
NSKeyedArchiver.archiveRootObject(myData, toFile: filePath)
if let Data = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? SensorOutput {
print (Data)
}
This gives an error during the process of archiving:
Error screenshot
PS: filePath I receiving in such way:
var filePath: String {
//1 - manager lets you examine contents of a files and folders in your app; creates a directory to where we are saving it
let manager = FileManager.default
//2 - this returns an array of urls from our documentDirectory and we take the first path
let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
print("this is the url path in the documentDirectory \(String(describing: url))")
//3 - creates a new path component and creates a new file called "Data" which is where we will store our Data array.
return (url!.appendingPathComponent("Data").path)
}
Reading/writing works with Int or Double and with other supported types, but not with my type. What’s wrong?
Although #matt's answer contains the essential information to solve your problem, it might not be obvious how to apply that information if you're new to Swift and iOS programming.
You tried using NSKeyedArchiver.archiveRootObject(_:toFile:), which is a class method, so you didn't have to create an instance of NSKeyedArchiver. Since encodeEncodable(_:forKey:) is an instance method, not a class method, you need to create an instance of NSKeyedArchiver to use it. You also need to create an NSMutableData for the archiver to append bytes to, and you have to call finishEncoding after encoding your object.
let sensorOutput = SensorOutput()
sensorOutput.timeStamp = Date()
let mutableData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: mutableData)
try! archiver.encodeEncodable(sensorOutput, forKey: NSKeyedArchiveRootObjectKey)
archiver.finishEncoding()
// You can now write mutableData to a file or send it to your server
// or whatever.
Similarly, you tried using NSKeyedUnarchiver.unarchiveObject(withFile:), which is a class method, but you need to use decodeDecodable(_:forKey:) or decodeTopLevelDecodable(_:forKey:), which are instance methods. So you need to read in the archive data and use it to make an instance of NSKeyedUnarchiver.
// Read in the data from a file or your server or whatever.
// I'll just make an immutable copy of the archived data for this example.
let data = mutableData.copy() as! Data
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
do {
if let sensorOutputCopy = try unarchiver.decodeTopLevelDecodable(SensorOutput.self, forKey: NSKeyedArchiveRootObjectKey) {
print("deserialized sensor output: \(sensorOutputCopy)")
}
} catch {
print("unarchiving failure: \(error)")
}
(I prefer the decodeTopLevelDecodable method instead of decodeDecodable because it throws a Swift error instead of crashing if the archive is corrupt.)
The error message is telling you that SensorOutput needs to derive from NSObject.
However, the root problem is that you are using NSKeyedArchiver wrong. You are calling archiveRootObject, acting as if this type adopted NSCoding. It doesn't. It adopts Codable. If you are going to use the fact that this type is Codable, you call NSKeyedArchiver encodeEncodable(_:forKey:) to encode and NSKeyedUnarchiver decodeDecodable(_:forKey:) to decode.
You were using it the wrong way. It adopts Codable protocol not NSCoding one. (As #matt pointed out).
You could solve it like that:
let myData = SensorOutput()
let data = try! PropertyListEncoder().encode(myData)
NSKeyedArchiver.archivedData(withRootObject: data)

Resources