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)
}
}
Related
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 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)
}
}
I have some json:
{
"wallets":[
{
"fundingProviderName":"tns_cof",
"authLimit":75,
"pinRequired":true,
"wallets":[
{
"authLimit":75,
"isCardSupported":true,
"paymentProcessorId":6,
"mainDisplayText":"...0013",
"imageUrl":"https://az755244.vo.msecnd.net/paymentimages/visa2.png",
"userPaymentSourceId":9756,
"secondaryDisplayText":"12/30",
"expDate":"2030-12-31T23:59:59Z",
"cardIssuer":"Visa"
},
{
"authLimit":75,
"isCardSupported":true,
"paymentProcessorId":7,
"mainDisplayText":"...1020",
"imageUrl":"https://az755244.vo.msecnd.net/paymentimages/mastercard2.png",
"userPaymentSourceId":9757,
"secondaryDisplayText":"12/25",
"expDate":"2025-12-31T23:59:59Z",
"cardIssuer":"Mastercard"
},
{
"authLimit":75,
"isCardSupported":true,
"paymentProcessorId":8,
"mainDisplayText":"...3025",
"imageUrl":"https://az755244.vo.msecnd.net/paymentimages/amex.png",
"userPaymentSourceId":9758,
"secondaryDisplayText":"12/27",
"expDate":"2027-12-31T23:59:59Z",
"cardIssuer":"Amex"
}
],
"isSupported":true
}
]
}
my struct looks like this:
struct CreditCard: Codable {
var authLimit: Int?
var isCardSupported: Bool?
var paymentProcessorId: Int?
var mainDisplayText: String?
var imageUrl: String?
var userPaymentSourceId: Int?
var secondaryDisplayText: String?
var expDate: String?
var cardIssuer: String?
enum CodingKeys: String, CodingKey {
case cardIssuer = "cardIssuer"
case authLimit = "authLimit"
case isCardSupported = "isCardSupported"
case paymentProcessorId = "paymentProcessorId"
case mainDisplayText = "mainDisplayText"
case imageUrl = "imageUrl"
case userPaymentSourceId = "userPaymentSourceId"
case secondaryDisplayText = "secondaryDisplayText"
case expDate = "expDate"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
authLimit = try values.decodeIfPresent(Int.self, forKey: .authLimit)
isCardSupported = try values.decodeIfPresent(Bool.self, forKey: .isCardSupported)
paymentProcessorId = try values.decodeIfPresent(Int.self, forKey: .paymentProcessorId)
mainDisplayText = try values.decodeIfPresent(String.self, forKey: .mainDisplayText)
imageUrl = try values.decodeIfPresent(String.self, forKey: .imageUrl)
userPaymentSourceId = try values.decodeIfPresent(Int.self, forKey: .userPaymentSourceId)
secondaryDisplayText = try values.decodeIfPresent(String.self, forKey: .secondaryDisplayText)
expDate = try values.decodeIfPresent(String.self, forKey: .expDate)
cardIssuer = try values.decodeIfPresent(String.self, forKey: .cardIssuer)
}
}
my json decoder code looks like this:
do {
if let jsonData = response?.responseData {
let jsonDecoder = JSONDecoder()
let creditCards = try jsonDecoder.decode(CreditCard.self, from: jsonData)
print("credit cards \(creditCards)")
completion(nil, creditCards)
}
} catch {
print(error)
}
I'm sure there's probably an obvious oversight to why the model is still all nil. Any help would be really appreciated. Thanks!
Either decode from the top of the JSON (as #Rob answered) or traverse through the JSON and get the wallets key and then decode it.
if let jsonData = response?.responseData as? Dictionary<String,Any> {
if let walletData = jsonData["wallet"] as? [Dictionary<String,Any>] {
if let mainWalletData = walletData[0]["wallets"] as? [Dictionary<String,Any>] {
do {
let jsonDecoder = JSONDecoder()
let creditCards = try jsonDecoder.decode([CreditCard].self, from: mainWalletData)
print("credit cards \(creditCards)")
completion(nil, creditCards)
}
} catch {
print(error)
}
}
}
}
struct Wallet: Decodable {
let wallets: [WalletDetails]
}
struct WalletDetails: Decodable {
let fundingProviderName: String
let authLimit: Int
let pinRequired: Bool
let wallets: [CreditCard]
}
struct CreditCard: Codable {
var authLimit: Int?
var isCardSupported: Bool?
var paymentProcessorId: Int?
var mainDisplayText: String?
var imageUrl: String?
var userPaymentSourceId: Int?
var secondaryDisplayText: String?
var expDate: String?
var cardIssuer: String?
enum CodingKeys: String, CodingKey {
case cardIssuer = "cardIssuer"
case authLimit = "authLimit"
case isCardSupported = "isCardSupported"
case paymentProcessorId = "paymentProcessorId"
case mainDisplayText = "mainDisplayText"
case imageUrl = "imageUrl"
case userPaymentSourceId = "userPaymentSourceId"
case secondaryDisplayText = "secondaryDisplayText"
case expDate = "expDate"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
authLimit = try values.decodeIfPresent(Int.self, forKey: .authLimit)
isCardSupported = try values.decodeIfPresent(Bool.self, forKey: .isCardSupported)
paymentProcessorId = try values.decodeIfPresent(Int.self, forKey: .paymentProcessorId)
mainDisplayText = try values.decodeIfPresent(String.self, forKey: .mainDisplayText)
imageUrl = try values.decodeIfPresent(String.self, forKey: .imageUrl)
userPaymentSourceId = try values.decodeIfPresent(Int.self, forKey: .userPaymentSourceId)
secondaryDisplayText = try values.decodeIfPresent(String.self, forKey: .secondaryDisplayText)
expDate = try values.decodeIfPresent(String.self, forKey: .expDate)
cardIssuer = try values.decodeIfPresent(String.self, forKey: .cardIssuer)
}
}
You have not added the top level keys and that is why it is not working. Try this:
do {
if let jsonData = response?.responseData {
let jsonDecoder = JSONDecoder()
let creditCards = try jsonDecoder.decode(Wallet.self, from: jsonData)
print("credit cards \(creditCards)")
completion(nil, creditCards)
}
} catch {
print(error)
}
top level key wallets is missing in your Codable, and you don't need to implement
init(from decoder: Decoder) throws
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
}
}
I have the following models:
struct Shop: Decodable {
let id: Int
let name: String
let products: [Product]
private enum CodingKeys: String, CodingKey {
case id
case name
case products
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.products = try container.decode([Product].self, forKey: .products)
}
}
struct Product: Decodable {
let id: Int
let title: String
let productImageURL: String
private(set) var productDetails: ProductDetails
private enum CodingKeys: String, CodingKey {
case id
case title
case productImageList
case productImageDetails
case productDetails
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
guard let serializationType = decoder.userInfo[.serializationType] as? SerializationGenericType else {
throw NetworkError.modelParsingError(message: "Product model missing serialization type")
}
switch serializationType {
case .list:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
case .details:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
self.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
}
}
}
Of course, the models are much more complex, but this is enough to explain my problem :)
Also, please don't mind the coding keys and the image URL, it's just to show that for different serialization type it initialized differently.
When getting the product details I have:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.details
let product = try decoder.decode(Product.self, from: data)
But when I try to pass the serialization type when initializing the Shop in the user info of the JsonDecoder like this:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.list
let shop = try decoder.decode(Shop.self, from: data)
In the Product initializer the user info is empty.
My question is how to propagate the serialization type from the Shop to the Product so I can properly decode it using the correct serialization type?