NSKeyedArchiver does not archive my Codable class - ios

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)

Related

Save a Codable to UserDefaults and matching the look of NSCoding

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
}
}

How to pull out data from different levels of JSON objects

so I have a json object that looks like this.
{
geometry = {
location = {
lat = "51.5194133";
lng = "-0.1269566";
};
};
id = ad6aaec7b7b0fa2c97a127c24845d76135e760ae;
"place_id" = ChIJB9OTMDIbdkgRp0JWbQGZsS8;
reference = "CmRRAAAAiC-ErdlAvz74Drejj2mAAh6Plr46e889a3Uv6CrRXFqNtVatoFsOTarDH0KU8KCkWoN--QGv01RSjLBZblbrAHNPGDVdiXikedid0vKMVM_LQtXstrSQFt4s-Z-Wi-1AEhDJRWc9bdWpKHPPOrt7QGTqGhSJgMPENn_wSGbprGYLv52csv5BtQ";
}
I was wondering how you can extract the information at different levels for example the location object is an object within the geometry objet and I want to extract lat from there how can I do this?
I can print out the location object like:
let setOne = jsonResult["results"]! as! NSArray
let y = setOne[0] as? [String: AnyObject]
print(y!)
print((y!["geometry"]!["location"]!)!["lat"])
but when I try to do:
print((y!["geometry"]!["location"]!)!["lat"])
it gives me the error: Type 'Any' has no subscript members
Perhaps the easiest way to do this is to use JSONDecoder to decode your JSON directly into structs.
You first need to define structs that match your JSON structure, like so:
struct Place: Codable {
let geometry: Geometry
let id: String
let place_id: String
let reference: String
}
struct Geometry: Codable {
let location: Location
}
struct Location: Codable {
let lat: String
let lng: String
}
Now, assuming that jsonResult["results"] is in fact an NSArray, you first need to convert it to Data and then use the JSONDecoder to decode the JSON into the structs:
if let data = try? JSONSerialization.data(withJSONObject: jsonResult["results"], options: []) {
if let places = try? JSONDecoder().decode(Array<Place>.self, from: data) {
print(places[0].geometry.location.lat) //prints "51.5194133"
}
}
The advantage of this approach is that you write much less code to do the actual decoding.
Note that if any of the JSON elements might be missing, you should declare the corresponding struct let property as an optional. For example, if reference might not always be present, you would code it as:
let reference: String?
Anything that is not optional must be present in the JSON, or the decode will fail. This is why you want to use try? when decoding, so that your app does not crash.
Lets say you have an array of results data, you don't need to use NSArray since you are using Swift so here you can start.
In Swift you need to specify the type, And also try not to use AnyObject unless you really need it, Always provide the type and thats what the error is saying:
// make sure that results exist
if let resultArray = jsonResult["results"] as? [Dictionary<String,Any>] {
// loop through the result array
for object in resultArray {
// get geometry from the object in the array
if let geometry = object["geometry"] as? Dictionary<String,Any>{
// make sure it has location
if let location = geometry["location"] as? Dictionary<String,Any>{
// now get the lat or lng safely.
let lat = location["lat"] as? String ?? "0.0"
let lng = location["lng"] as? String ?? "0.0"
}
}
}
}
Note: Never use force-unwrap ! because your app will crash if something went wrong or that specific object is not found, make sure to use guard let or if let at least, although this is manual parsing you can look into Swift 4 new Encoding, Decoding and Serialization in Swift 4.
Considering you have an array of data as results (jsonResult), start accessing your array and identify the type of each parameter.
if let resultArray = jsonResult["results"] as? NSArray {
for aResult in resultArray {
//using "if let" simply means, in this case, if "geometry" is indeed a dictionary it will execute the statement/s inside this "if" block.
if let geometry = aResult["geometry"] as? NSDictionary {
if let location = geometry["location"] as? NSDictionary {
if let latValue = location["lat"] as? String {
//This will only be executed if the value is String
print(latValue)
}
if let lngValue = location["lng"] as? String {
print(lngValue)
}
}
}
}
}

How to implement model class for multiple values in swift 3?

Here I am having value in JSON in which for some of multiple key value pairs it returning string and for some it is returning array here in custom attributes array in first dictionary in that value key value pair the data present is different and in the second dictionary value key value pair is different here then how to implement the model class for inside array for different key values ?
struct MediaGallery {
let id : Int
let mediaType : String
let label : Any
let position : Int
let disabled : Any
let file : String
init(dict : [String:Any]) {
self.id = (dict["id"] as? Int)!
self.mediaType = (dict["media_type"] as? String)!
self.label = dict["label"]!
self.position = (dict["position"] as? Int)!
self.disabled = dict["disabled"]!
self.file = (dict["file"] as? String)!
}
}
struct AttributeList {
let label : String
let value : String
let code : String
init(dict : [String:Any]){
self.label = (dict["label"])! as! String
self.value = (dict["value"])! as! String
self.code = (dict["code"])! as! String
}
}
struct DetailsListAttribute {
let attributeCode : String
let value : Any
init?(dict : [String:Any]) {
self.attributeCode = dict["attribute_code"] as! String
print(self.attributeCode)
if let values = dict["value"] as? String {
self.value = values
}
else {
if let arr = dict["value"] as? [[String:Any]]{
var filterArr = [AttributeList]()
for obj in arr {
filterArr.append(AttributeList(dict: obj))
}
self.value = filterArr
} else {
self.value = [AttributeList]()
}
}
}
}
I would suggest please save some time by using this great GIT Library ObjectMapper . it will help you to model your object and convert your model objects (classes and structs) to JSON and vice versa.
I've tried multiple JSON-mapping frameworks that were mentioned in Tj3n comment. They all have pros and cons. Apple suggests you to follow the recommendation given here. Also you should check Codable protocol (swift 4 is required).
Ok I don't have the whole JSON, and it doesn't seem clear to me.
But here is how you can parse and create your model Class easily in Swift with the Codable protocol.
You can read more about it and/or some examples, tutorials : Ultimate Guide.
Briefly, what is the Codable protocol ?
You don't need third party library anymore in order to parse and set the json data to your model class.
You juste have to create your class like the JSON is represented. And according to the key-name, it will create the class, properties and everything for you.
Here is an example with your JSON, I don't know if I understood your JSON formatting, but you got the trick :
struct Response: Codable {
let ca: [CustomAttribute]?
enum CodingKeys: String, CodingKey {
case ca = "custom_attributes"
}
}
struct CustomAttribute: Codable {
let code: String?
let value: [Value]?
struct Value: Codable {
let label: String?
let value: String?
let code: String?
let avg: String? // I don't know how your value array is composed
let count: Int? // I don't know how your value array is composed
}
enum CodingKeys: String, CodingKey {
case code = "attribute_code"
case avg = "avg_rating_percent"
}
}
For me, it looks like something like that.
I don't see the whole JSON, but imagine you have the whole JSON as the Response Struct, it contains several objects, like the CustomAttribute Array for example.
Then you can define the CustomAttribute structure, and add as many properties as the JSON has.
Anyway, you can call it this way :
When you have the response from your API call, you can go :
if let data = response.data {
let decoder = JSONDecoder()
let response = try! decoder.decode(Response.self, from: data)
print("Only printing the Custom Attribute : \(response.ca!)")
}
I decode the whole json data as an Object Response (like my Struct).
And I pass to my response callback, or
this might be late but I think this will helps others
The model class which are varies for frameworks like SwiftyJSON, simple swift class, Gloss or swift codable (Swift 4). you can easily generate model class online with your customization jsoncafe.com

Storing and Retrieving Image in Sqlite with swift

How to store and fetch images in SQLite and in what format the images get saved? It would be more helpful if explained with an example.
Image itself cannot be stored into a database columns but you can first convert it into a string and then store it. The string is called base64 string. As far as I know, any image can be converted to that and reversely.
To encode to base 64:
let image : UIImage = UIImage(named:"imageNameHere")!
let imageData:NSData = UIImagePNGRepresentation(image)!
let strBase64 = imageData.base64EncodedString(options: .lineLength64Characters)
Now your UIImage object is converted to a String! Save strBase64 to SQLite DB. Remember to use text as column type because this string is very long.
To decode back to UIImage:
let dataDecoded:NSData = NSData(base64EncodedString: strBase64, options: NSDataBase64DecodingOptions(rawValue: 0))!
let decodedimage:UIImage = UIImage(data: dataDecoded)!
Alternative
save your image into document directory.
save your image file path or name of image in sqlite
Get the image path or name from sqlite and access that path from document directory.
Take Ref : Iphone : How to Display Document Directory images in Image View?
You can also store your image directly as a BLOB, however it depends on which framework you use for SQLite access. In case you use SQLite.swift, then there is an option:
Set up a file SQLiteHelper.swift like that:
class SQLiteHelper{
var db: Connection!
let personsTable = Table("person")
let id = Expression<Int>("id")
let firstName = Expression<String>("firstName")
let lastName = Expression<String>("lastName")
let profileImage = Expression<Data>("profileImage")
let date = Expression<Date>("savedAt")
init() {
do{
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let dbTemp = try Connection("\(path)/myDb.sqlite3") //Create db if not existing
self.db = dbTemp
}
catch {
print("Error info: \(error)")
}
}
public func insertData(firstNameVal: String,
lastNameVal: String,
profileImageVal: Data,
dateVal: Date){
do{
//Create a new table only if it does not exist yet
try db.run(personsTable.create(ifNotExists: true) { t in // CREATE TABLE "person" (
t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL,
t.column(firstName) // "firstName" TEXT,
t.column(lastName) // "lastName" TEXT,
t.column(profileImage) // "profileImage" BLOB,
t.column(date) // "savedAt" DATETIME)
})
}
catch {
print("The new SQLite3 Table could not be added: \(error)")
}
do{
try db.run(personsTable.insert(firstName <- firstNameVal,
lastName <- lastNameVal,
profileImage <- profileImageVal,
date <- dateVal
))
}
catch {
print("Could not insert row: \(error)")
}
}
public func getData() -> [Person]{
var persons = [Person]()
do{
for row in try db.prepare(personsTable) {
let person: Person = Person(firstName: row[firstName],
lastName: row[lastName],
profileImage: row[profileImage],
savedAt: row[date])
persons.append(person)
}
}
catch {
print("Could not get row: \(error)")
}
return persons
}
Now create a file Person.swift and put the following struct inside of it:
import Foundation
struct Person: Identifiable {
var id = UUID()
var firstName: String
var lastName: String
var profileImage: Data
var savedAt: Date
}
Store Data
In order to store data as a .png BLOB you would now basically do something like that:
var databaseHelper: SQLiteHelper = SQLiteHelper.init()
self.databaseHelper.insertData(firstNameVal: "yourFirstName",
lastNameVal: "yourLastName",
profileImageVal: yourImageView.pngData(),
dateVal: Date())
Retreive Data
If you want to display the image later in another Imageview you would have to do this:
var persons = self.databaseHelper.getData()
let profileImage = UIImage(data: persons[0].profileImage)
let myImageView = UIImageView(image: profileImage)
Saving UIImage as BLOB
I have saved the image as a .png because I want to use my database outside of iOS and therefore want to ensure compatibility. If you want you can also store your UIImage directly. You would roughly need to change it like that:
let profileImage = Expression<UIImage>("profileImage")
...
profileImageVal: yourImageView,
...
let myImageView = persons[0].profileImage
...
import Foundation
import UIKit
struct Person: Identifiable {
var id = UUID()
var firstName: String
var lastName: String
var profileImage: UIImage
var savedAt: Date
}
Note: SQLite.swift also supports lazy loading, which would probably make more sense in ascenario like that...

whats the most efficient way to store(persist) array of structure before the application terminates?

Programming language - swift.
I have created a structure to store all relevant information pertaining to my app as follows. Further, I am creating an array of type wordstruct and populating it with come 3000 entries.
struct wordStruct{
var wordd:NSString
var meaning: [NSString]
var sentance: String
var syn: [String]
var seen:Int
var set: Int
var HF: Int
}
After trying multiple methods that I could think of, I haven't been able to store(persist) the data(i.e array of structure).
I have tried NSUserDefault, Plist and NSCoding methods for the same (I may have been doing something wrong.)
what is the most efficient way to store an array of structure ?
can writing the array to a file do the trick ?
Structs cannot conform to NSCoding, as NSCoding is a class protocol. If you want to use NSCoding to persist your model (it is typically the most straightforward approach), you will have to redeclare your model as an NSObject that conforms to NSCoding.
Here is an example of how you could implement it based on the snippet you provided:
class Word: NSObject, NSCoding {
var word: NSString!
var meaning: [NSString]!
var sentence: String!
var syn: [String]!
var seen:Int!
var set: Int!
var HF: Int!
// specify how your model will be decoded:
required convenience init(coder decoder: NSCoder) {
self.init()
self.word = decoder.decodeObjectForKey("word") as! String
self.meaning = decoder.decodeObjectForKey("meaning") as! [String]
self.sentence = decoder.decodeObjectForKey("sentence") as! String
self.syn = decoder.decodeObjectForKey("syn") as! [String]
self.seen = decoder.decodeIntegerForKey("seen")
self.set = decoder.decodeIntegerForKey("set")
self.HF = decoder.decodeIntegerForKey("HF")
}
// specify how your model will be encoded
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(word, forKey: "word")
coder.encodeObject(meaning, forKey: "meaning")
coder.encodeObject(sentence, forKey: "sentence")
coder.encodeObject(syn, forKey: "syn")
coder.encodeInteger(seen, forKey: "seen")
coder.encodeInteger(set, forKey: "set")
coder.encodeInteger(HF, forKey: "HF")
}
}
// an example word:
let example = Word()
example.word = "example"
example.meaning = ["abstractions", "generalizations", "use cases"]
example.sentence = "here is how you can implement NSCoding"
example.syn = ["what", "is", "a", "syn"]
example.seen = 1
example.set = 2
example.HF = 3
// persist the example to NSUserDefaults
let data = NSKeyedArchiver.archivedDataWithRootObject(example)
NSUserDefaults.standardUserDefaults().setObject(data, forKey: "example")
// load the saved example from NSUserDefaults
if let data = NSUserDefaults.standardUserDefaults().objectForKey("example") as? NSData {
if let example = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? Word {
example.meaning // ["abstractions", "generalizations", "use cases"]
example.set // 2
example.seen // 1
example.word // "example"
}
}
For further reading, Matt Thompson has an excellent post on the subject here:
http://nshipster.com/nscoding/
And it never hurts to read the official documentation:
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/index.html
Wrap the structure in 'NSData' first usually does the trick for 'NSCoding' as it is then just treated as a sequence of bytes.
Or use 'Core Data' right away to make your life easier in the long run in regards to persistent storage of information.

Resources