How to convert an array of custom classes to Data? - ios

I want to convert an array of custom Note() classes to Data() to encrypt it later on and store it.
My class is
class Note: NSObject {
var text = "hi"
var date = Date()
}
The class above is an example. My class would be a bit more complex.
I've tried
let plistData = NSKeyedArchiver.archivedData(withRootObject: mergedNotes)
if let a: Array<Note> = NSKeyedUnarchiver.unarchiveObject(with: plistData) as? Array<Note> {
print(a)
}
The above code crashes with a signal sigabrt.
How does this work and is it possible without encoding an decoding the entire class?

First of all setup your Note class, must conform to the codable protocol:
class Note: Codable {
var text = "hi"
var date = Date()
init(t: String, d: Date) {
text = t
date = d
}
enum CodingKeys: String, CodingKey {
case text
case date
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
text = try container.decode(String.self, forKey: .text)
date = try container.decode(Date.self, forKey: .date)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(text, forKey: .text)
try container.encode(date, forKey: .date)
}
}
Then you can create your array of notes:
let arrayOfNotes: [Note] = [Note(t: "someNote", d: Date())]
And just save it in user defaults:
do {
let encoded = try JSONEncoder().encode(arrayOfNotes)
UserDefaults.standard.set(encoded, forKey: "SavedNotes")
} catch let error {
// catch any error if present
print(error)
}
to retrive your saved data you can simply use an if let or a do-catch like above:
if let savedData = UserDefaults.standard.value(forKey: "SavedNotes") as? Data {
if let notes = try? JSONDecoder().decode(Note.self, from: savedData) {
// do whatever you need
}
}

Related

Decoding json with optional fields

I am trying to parse a json string in which some of the keys are not fixed. There are some keys which are related to error and either it will be error or the result data. Followings are the two examples:
{
"ok": true,
"result": {
"code": "694kyH",
"short_link": "shrtco.de\/694kyH",
"full_short_link": "https:\/\/shrtco.de\/694kyH",
"short_link2": "9qr.de\/694kyH",
"full_short_link2": "https:\/\/9qr.de\/694kyH",
"short_link3": "shiny.link\/694kyH",
"full_short_link3": "https:\/\/shiny.link\/694kyH",
"share_link": "shrtco.de\/share\/694kyH",
"full_share_link": "https:\/\/shrtco.de\/share\/694kyH",
"original_link": "http:\/\/google.com"
}
}
{
"ok": false,
"error_code": 2,
"error": "This is not a valid URL, for more infos see shrtco.de\/docs"
}
How will I parse this JSON. I have tried to build my class like following but it is not working:
struct ShortLinkData: Codable {
let ok: Bool
let result: Result?
let errorCode: Int?
let error: String?
private enum CodingKeys : String, CodingKey { case ok, result, errorCode = "error_code", error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
ok = try container.decode(Bool.self, forKey: .ok)
result = try container.decode(Result.self, forKey: .result)
errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
// MARK: - Result
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decode(String.self, forKey: .code)
shortLink = try container.decode(String.self, forKey: .shortLink)
fullShortLink = try container.decode(String.self, forKey: .fullShortLink)
shortLink2 = try container.decode(String.self, forKey: .shortLink2)
fullShortLink2 = try container.decode(String.self, forKey: .fullShortLink2)
shortLink3 = try container.decode(String.self, forKey: .shortLink3)
fullShortLink3 = try container.decode(String.self, forKey: .fullShortLink3)
shareLink = try container.decode(String.self, forKey: .shareLink)
fullShareLink = try container.decode(String.self, forKey: .fullShareLink)
originalLink = try container.decode(String.self, forKey: .originalLink)
}
}
My parsing code:
let str = String(decoding: data, as: UTF8.self)
print(str)
let shortURL = try? JSONDecoder().decode(ShortLinkData.self, from: data)
return shortURL!
I am always getting nil in shortURL object.
You should split this into several steps in order to avoid to handle all these optionals in your model.
First create a struct that has only those properties that are guaranteed to be there. ok in your case:
struct OKResult: Codable{
let ok: Bool
}
then create one for your error state and one for your success state:
struct ErrorResult: Codable{
let ok: Bool
let errorCode: Int
let error: String
private enum CodingKeys: String, CodingKey{
case ok, errorCode = "error_code", error
}
}
struct ShortLinkData: Codable {
let ok: Bool
let result: Result
}
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
}
Then you can decode the data:
guard try JSONDecoder().decode(OKResult.self, from: data).ok else{
let errorResponse = try JSONDecoder().decode(ErrorResult.self, from: data)
//handle error scenario
fatalError(errorResponse.error) // or throw custom error or return nil etc...
}
let shortlinkData = try JSONDecoder().decode(ShortLinkData.self, from: data)
Remarks:
Your inits are not necessary.
Never use try? this will hide all errors from you
you would need to wrap this either in a do catch block or make your function throwing and handle errors further up the tree.
Actually there are no optional fields. The server sends two different but distinct JSON strings.
A suitable way to decode both JSON strings is an enum with associated values. It decodes the ok key, then it decodes either the result dictionary or errorCode and error
enum Response : Decodable {
case success(ShortLinkData), failure(Int, String)
private enum CodingKeys : String, CodingKey { case ok, result, errorCode, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let ok = try container.decode(Bool.self, forKey: .ok)
if ok {
let result = try container.decode(ShortLinkData.self, forKey: .result)
self = .success(result)
} else {
let errorCode = try container.decode(Int.self, forKey: .errorCode)
let error = try container.decode(String.self, forKey: .error)
self = .failure(errorCode, error)
}
}
}
In ShortLinkData the init method and the CodingKeys are redundant if you specify the convertFromSnakeCase key decoding strategy
struct ShortLinkData: Decodable {
let code, shortLink: String
let fullShortLink: String
let shortLink2, fullShortLink2: String
let shortLink3, fullShortLink3: String
let shareLink, fullShareLink: String
let originalLink: String
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Response.self, from: data)
switch result {
case .success(let linkData): print(linkData)
case .failure(let code, let message): print("An error occurred with code \(code) and message \(message)")
}
} catch {
print(error)
}

Saving codable struct in Userdefaults returns nil values upon fetching

Saving data in defaults:
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: DefaultKeys.user)
defaults.synchronize()
}
fetching data but getting nil keys of struct.
let defaults = UserDefaults.standard
if let savedPerson = defaults.object(forKey: DefaultKeys.user) as? Data {
let decoder = JSONDecoder()
if let loadedPerson = try? decoder.decode(UserModel.self, from: savedPerson) {
return loadedPerson
}
}
I tried to google the approaches everyone suggest the same to save custom object in userdefaults, but the approach is not working for me.Given below is my struct model which conform Codable as well.
struct UserModel : Codable {
var userDetails : UserDetail?
var status : Bool?
var message : String?
enum CodingKeys: String, CodingKey {
case userDetails = "userDetails"
case status = "status"
case message = "message"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
userDetails = try UserDetail(from: decoder)
status = try values.decodeIfPresent(Bool.self, forKey: .status)
message = try values.decodeIfPresent(String.self, forKey: .message)
}
}

Swift - Saving an Object with Codeable crashes app

I'm writing an app where I am trying to save an array inside an array via Codable and into UserDefaults, but it crashes, with the following error:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[Here.UserEntries
encodeWithCoder:]: unrecognized selector sent to instance
0x600000649e70'
Here's how I'm saving it:
class UserEntries: NSObject, Codable {
struct Question : Codable {
var question: String
var answer: String
enum CodingKeys: String, CodingKey {
case question
case answer
}
init(question: String, answer: String) {
self.question = question
self.answer = answer
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(question, forKey: .question)
try container.encode(answer, forKey: .answer)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
question = try container.decode(String.self, forKey: .question)
answer = try container.decode(String.self, forKey: .answer)
}
}
var date: String
var questions: [Question]
enum CodingKeys: String, CodingKey {
case date
case questions
}
init(date: String, questions: [Question]) {
self.date = date
self.questions = questions
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(date, forKey: .date)
try container.encode(questions, forKey: .questions)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try container.decode(String.self, forKey: .date)
questions = [try container.decode(Question.self, forKey: .questions)]
}
}
where userEntry is:
let userEntry = UserEntries(date: String(todayDate), questions: [UserEntries.Question(question: q1Text, answer: q1Answer), UserEntries.Question(question: q2Text, answer: q2Answer)])
then
UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: userEntry), forKey: "allEntries")
What exactly is going wrong? I have a feeling that I can't save an array inside an array, but then how can this be fixed?
This line:
questions = [try container.decode(Question.self, forKey: .questions)]
is incorrect. To get array of Question you should decode type of array [Question].self:
questions = try container.decode([Question].self, forKey: .questions)
Also, note that Decodable/Encodable works with JSONDecoder/JSONEncoder, see official docs. According to this, here is what you need to save (encode to data and then save):
let jsonEncoder = JSONEncoder()
if let value = try? jsonEncoder.encode(userEntry) {
UserDefaults.standard.set(value, forKey: "allEntries")
}
And the opposite - get data and if it is, try to decode the object:
let jsonDecoder = JSONDecoder()
if let data = UserDefaults.standard.data(forKey: "allEntries"),
let userEntry = try? jsonDecoder.decode(UserEntries.self, from: data) {
// here you get userEntry
}
Example:
let userEntry = UserEntries(date: "1 Sep 18",
questions: [
UserEntries.Question(question: "q1Text", answer: "q1Answer"),
UserEntries.Question(question: "q2Text", answer: "q2Answer")
])
let jsonEncoder = JSONEncoder()
if let value = try? jsonEncoder.encode(userEntry) {
UserDefaults.standard.set(value, forKey: "allEntries")
}
let jsonDecoder = JSONDecoder()
if let data = UserDefaults.standard.data(forKey: "allEntries"),
let userEntry = try? jsonDecoder.decode(UserEntries.self, from: data) {
print(userEntry.date) // 1 Sep 18
}
To set the model into UserDefaults please try this
if let encoded = try? JSONEncoder().encode(value) {
UserDefaults.standard.set(encoded, forKey: "allEntries")
}
For decoding:
if let data = UserDefaults.standard.value(forKey: "allEntries") as? Data {
if let yourData = try? JSONDecoder().decode(UserEntries.self, from: data) {
Print(yourData)
}
}

How do type adjust uing JSONDecoder in swift?

I define a model like this:
struct DMTest: Codable {
var uid: Int
var name: String?
}
and do the model decode like this:
let jsonStr = "{\"uid\":123,\"name\":\"haha\"}"
let jsonData = jsonStr.data(using: .utf8)!
do {
let decoder = JSONDecoder()
let result = try decoder.decode(DMTest.self, from:jsonData)
XCGLogger.debug("result = \(result)")
}catch {
XCGLogger.debug("error")
}
When jsonStr is like below, it works well:
{"uid":123,"name":"haha"}
when jsonStr is like below, it will throw a exception:
{"uid":"123","name":"haha"}
It means that if the type of the "uid" not adapter, it can't be decode. But some times the framework of server is weak type, it may give me dirty data like this, How can I adjust the type?
For example: I define Int in the model, if the server give some data of String type and I can covert it to Int, just decode to Int otherwise throw ecxeption.
You can define a custom parsing solution:
struct DMTest: Codable {
enum CodingKeys: String, CodingKey {
case uid, name
}
var uid: Int
var name: String?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringUID = try? container.decode(String.self, forKey: .uid), let intUID = Int(stringUID) {
uid = intUID
} else if let intUID = try? container.decode(Int.self, forKey: .uid) {
uid = intUID
} else {
uid = 0 // or throw error
}
name = try container.decodeIfPresent(String.self, forKey: .name)
}
}

Class conforming to Codable protocol fails with encodeWithCoder: unrecognized selector sent to instance

I know that there are several questions similar to this, that tend to all revolve around the class not conforming to the protocol properly, but that should not be the immediate issue here.
The following is a condensed version of the code that is currently giving me this problem:
enum Binary: Int {
case a = 0
case b = 1
case c = 9
}
final class MyClass: NSCoder {
var string: String?
var date: Date?
var binary: Binary = .c
override init() { }
enum CodingKeys: CodingKey {
case string, date, binary
}
}
extension MyClass: Codable {
convenience init(from decoder: Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
string = try values.decode(String.self, forKey: .string)
date = try values.decode(Date.self, forKey: .date)
binary = try Binary(rawValue: values.decode(Int.self, forKey: .binary)) ?? .c
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(string, forKey: .string)
try container.encode(date, forKey: .date)
try container.encode(binary.rawValue, forKey: .binary)
}
}
I have created the following class which then attempts to call MyClass with the purpose of writing & reading it to UserDefaults:
class MyClassController {
private let myClass: MyClass
init() {
self.myClass = MyClass()
self.myClass.string = "string"
self.myClass.date = Date()
self.myClass.binary = .a
}
func writeMyClass() {
let encodedData = NSKeyedArchiver.archivedData(withRootObject: myClass)
UserDefaults.standard.set(encodedData, forKey: String(describing: MyClass.self))
}
func readMyClass() {
if let decoded = UserDefaults.standard.object(forKey: String(describing: MyClass.self)) as? Data,
let myClass = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data) as? MyClass {
print("string: \(myClass.string ?? "nil") date: \(myClass.date ?? Date()) binary: \(myClass.binary)")
}
}
}
As soon as I call the writeMyClass function though, I get this error:
[DemoDecoder.MyClass encodeWithCoder:]: unrecognized selector sent to
instance #blahblah#
Two things I have also tried:
Adding func encode(with aCoder: NSCoder) to MyClass
Removed all properties from MyClass & CodingKeys and the init/encode functions
You have a lot of mismatched attempts and various encoding/decoding mechanisms.
NSKeyedArchiver and NSKeyedUnarchiver require that all involved types conform to the NSCoding protocol. This is the older mechanism from the Objective-C frameworks.
The protocols Codable, Encoder, and Decoder are new to Swift 4. Such data types should be used with Swift encoder and decoders such as JSONEncoder and JSONDecoder or PropertyListEncoder and PropertyListDecoder.
I suggest you remove the reference to NSCoder and remove the uses of NSKeyedArchiver and NSKeyedUnarchiver. Since you have implemented the Codable protocol, use an appropriate Swift encoder and decoder. In your case you want to use PropertyListEncoder and PropertyListDecoder.
Once that is done you should probably change MyClass to be a struct instead of a class.
You should also avoid use UserDefaults to store data. Write the encoded data to a plist file instead.
This is the working code derived from the answer provided by rmaddy above.
A few highlights:
Convert MyClass to MyStruct
Removed NSCoder inheritance from the object I wished to save
Removed calls to NSKeyedArchiver & NSKeyedUnarchiver
No longer saving to UserDefaults
Relying on JSONEncoder & JSONDecoder to write out struct
Writing to file system now as a Data object
This is the updated struct & enum that I wish to save:
enum Binary: Int {
case a = 0
case b = 1
case c = 9
}
struct MyStruct {
var string: String?
var date: Date?
var binary: Binary = .c
init() { }
enum CodingKeys: CodingKey {
case string, date, binary
}
}
extension MyStruct: Codable {
init(from decoder: Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
string = try values.decode(String.self, forKey: .string)
date = try values.decode(Date.self, forKey: .date)
binary = try Binary(rawValue: values.decode(Int.self, forKey: .binary)) ?? .c
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(string, forKey: .string)
try container.encode(date, forKey: .date)
try container.encode(binary.rawValue, forKey: .binary)
}
}
The updated controller class that handles reading & writing the output. In my case, writing out to JSON was fine, so I went with that approach.
class MyStructController {
private var myStruct: MyStruct
init() {
self.myStruct = MyStruct()
self.myStruct.string = "string"
self.myStruct.date = Date()
self.myStruct.binary = .a
}
func writeMyStruct() {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(myStruct)
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
try data.write(to: url)
} catch {
print(error.localizedDescription)
}
}
func readMyStruct() {
do {
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let myNewStruct = try decoder.decode(MyStruct.self, from: data)
print("string: \(myNewStruct.string ?? "nil") date: \(myNewStruct.date ?? Date()) binary: \(myNewStruct.binary)")
} catch {
print(error.localizedDescription)
}
}
}
Solution from #CodeBender works just fine, though there is no need to do manual encoding / decoding using init(from decoder: Decoder) and encode(to encoder: Encoder) methods, doing so just defeats the very purpose of the GREAT Codable protocol, unless you need to do some complex level of encoding / decoding.
Here is the code that works just well using the pure benefit of Codable protocol:
import UIKit
struct Movie: Codable {
enum MovieGenere: String, Codable {
case horror, drama, comedy, adventure, animation
}
var name : String
var moviesGenere : [MovieGenere]
var rating : Int
}
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
writeMyMovie(movie: Movie(name: "Titanic", moviesGenere: [Movie.MovieGenere.drama], rating: 1))
readMyMovie()
}
var documentDirectoryURL:URL? {
do {
let documentDirectory = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:false)
return documentDirectory.appendingPathComponent(String(describing: Movie.self))
} catch {
return nil
}
}
func writeMyMovie(movie:Movie) {
do {
let data = try JSONEncoder().encode(movie)
try data.write(to: documentDirectoryURL!) // CAN USE GUARD STATEMENT HERE TO CHECK VALID URL INSTEAD OF FORCE UNWRAPPING, IN MY CASE AM 100% SURE, HENCE NOT GUARDING ;)
} catch {
print(error.localizedDescription)
}
}
func readMyMovie() {
do {
let data = try Data(contentsOf: documentDirectoryURL!)
let movie = try JSONDecoder().decode(Movie.self, from: data)
print("MOVIE DECODED: \(movie.name)")
} catch {
print(error.localizedDescription)
}
}
}

Resources