Encoding generic String object gives nil Swift - ios

I have a UserDefaults class that handles storing, deleting, and fetching of stored objects to defaults. Here's the complete class, neat and simple, I believe:
Now the problem lies in storing function. I couldn't seem to encode an Encodable String object. I know I could just store that object to the defaults, but that would defeat the purpose of this MVDefaults that deals with generic objects.
Anything I'm missing here?
import Foundation
enum MVDefaultsKey: String {
case requestToken = "defaultsRequestToken"
}
/// The class that has multiple class functions for handling defaults.
/// Also has the helper class functions for handling auth tokens.
class MVDefaults {
// MARK: - Functions
/// Stores token.
class func store<T: Encodable>(_ object: T, key: MVDefaultsKey) {
let encoder = JSONEncoder()
let encoded = try? encoder.encode(object)
UserDefaults.standard.set(encoded, forKey: key.rawValue)
}
/// Removes the stored token
class func removeDefaultsWithKey(_ key: MVDefaultsKey) {
UserDefaults.standard.removeObject(forKey: key.rawValue)
}
/// Returns stored token (optional) if any.
class func getObjectWithKey<T: Decodable>(_ key: MVDefaultsKey, type: T.Type) -> T? {
guard let savedData = UserDefaults.standard.data(forKey: key.rawValue) else {
return nil
}
let object = try? JSONDecoder().decode(type, from: savedData)
return object
}
}

Think about what would the string "hello" encoded to JSON, look like. It would just look like:
"hello"
wouldn't it?
That is not a valid JSON (according to here)! You can't encode a string directly to JSON, and you can't decode a string directly either.
For example, this code
let string = try! JSONDecoder().decode(String.self, from: "\"hello\"".data(using: .utf8)!)
will produce the error
JSON text did not start with array or object and option to allow fragments not set.
And
let data = try! JSONEncoder().encode("Hello")
will produce the error:
Top-level String encoded as string JSON fragment.
The work around here is just to store your strings with the dedicated set methods provided by UserDefaults. You can still have your generic method, though, you just need to check the type and cast:
if let str = object as? String {
UserDefaults.standard.set(str, forKey: key)
} else if let int = object as? Int {
...

Whilst Sweeper's comment is helpful and should be the answer (since I won't be able to come up to my own answer without his), I'll still go ahead and share what I did to my Defaults class, to make it handle the non JSON encoding objects (e.g. String, Int, Array, and whatnot).
Here it is:
MVDefaults.swift
import Foundation
enum MVDefaultsKey: String {
case requestToken = "defaultsRequestToken"
case someOtherKey = "defaultsKeyForTests"
}
/// The class that has multiple class functions for handling defaults.
/// Also has the helper class functions for handling auth tokens.
class MVDefaults {
// MARK: - Functions
/// Stores object.
class func store<T: Encodable>(_ object: T, key: MVDefaultsKey) {
let encoder = JSONEncoder()
let encoded = try? encoder.encode(object)
if encoded == nil {
UserDefaults.standard.set(object, forKey: key.rawValue)
return
}
UserDefaults.standard.set(encoded, forKey: key.rawValue)
}
/// Removes the stored object
class func removeDefaultsWithKey(_ key: MVDefaultsKey) {
UserDefaults.standard.removeObject(forKey: key.rawValue)
}
/// Returns stored object (optional) if any.
class func getObjectWithKey<T: Decodable>(_ key: MVDefaultsKey, type: T.Type) -> T? {
if let savedData = UserDefaults.standard.data(forKey: key.rawValue) {
let object = try? JSONDecoder().decode(type, from: savedData)
return object
}
return UserDefaults.standard.object(forKey: key.rawValue) as? T
}
}
And here's the test (spec) that I wrote for testing the Defaults methods. All passed! ✅
MVDefaultsSpec.swift
#testable import Movieee
import Foundation
import Quick
import Nimble
class MVDefaultsSpec: QuickSpec {
override func spec() {
describe("Tests for MVDefaults") {
context("Tests the whole MVDefaults.") {
it("tests the store and retrieve function for any Codable object") {
let data = stubbedResponse("MovieResult")
expect(data).notTo(beNil())
let newMovieResult = try? JSONDecoder().decode(MovieResult.self, from: data!)
MVDefaults.store(newMovieResult, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: MovieResult.self)
// Assert
expect(retrievedObject).notTo(beNil())
// Assert
expect(retrievedObject?.movies?.first?.id).to(equal(newMovieResult?.movies?.first?.id))
}
it("tests the store and retrieve function for String") {
let string = "Ich bin ein Berliner"
MVDefaults.store(string, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: String.self)
// Assert
expect(retrievedObject).notTo(beNil())
// Assert
expect(retrievedObject).to(equal(string))
}
it("tests the removal function") {
MVDefaults.removeDefaultsWithKey(.someOtherKey)
let anyRetrievedObject = UserDefaults.standard.object(forKey: MVDefaultsKey.someOtherKey.rawValue)
let stringRetrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: String.self)
// Assert
expect(anyRetrievedObject).to(beNil())
// Assert
expect(stringRetrievedObject).to(beNil())
}
it("tests the store and retrieve function for Bool") {
let someBool: Bool = true
MVDefaults.store(someBool, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: Bool.self)
// Assert
expect(retrievedObject).to(equal(someBool))
// Assert
expect(retrievedObject).notTo(beNil())
}
}
}
}
}

Related

How to archive data in swift?

I am trying to archive data and want to store it in userdefault but app getting crash.
Also tried this
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: selectedPoductDetails, requiringSecureCoding: false)
selectedPoductDetails is dict of type [String: SelectedProductDetail]
import Foundation
class SelectedProductDetail {
let product: String
var amount: Double
var title: String
init(product: String, amount: Double, title: String ) {
self.product = product
self.amount = amount
self.title = title
}
}
May i know why its not working and possible solution for same?
For this case you can use UserDefaults
struct ProductDetail: Codable {
//...
}
let encoder = JSONEncoder()
let selectedProductDetails = ProductDetail()
// Set
if let data = try? encoder.encode(selectedProductDetails) {
UserDefaults.standard.set(data, forKey: "selectedProductDetails")
}
// Get
if let selectedProductDetailsData = UserDefaults.standard.object(forKey: "selectedProductDetails") as? Data {
let selectedProductDetails = try? JSONDecoder().decode(ProductDetail.self, from: selectedProductDetailsData)
}
As mentioned in the comments to use NSKeyedArchiver the class must adopt NSSecureCoding and implement the two required methods.
The types in your class are JSON compatible, so adopt Codable and archive the data with JSONEncoder (or PropertyListEncoder). You could even use a struct and delete the init method
struct SelectedProductDetail: Codable {
let product: String
var amount: Double
var title: String
}
var productDetails = [String: SelectedProductDetail]()
// populate the dictionary
do {
let data = try JSONEncoder().encode(productDetails)
UserDefaults.standard.set(data, forKey: "productDetails")
} catch {
print(error)
}
And load it
do {
guard let data = UserDefaults.standard.data(forKey: "productDetails") else { return }
productDetails = try JSONDecoder().decode([String: SelectedProductDetail].self, from: data)
} catch {
print(error)
}
Note:
UserDefaults is the wrong place for user data. It's better to save the data in the Documents folder

How to compare two JSON objects in Swift?

I have an json object and store it as initialData and after some changes in store the json object into another modifiedData. Now I am trying to compare two json object of initialData and modifiedData but i could not able to compare it.
Note: Here json object are dynamic value.
Sample code:
let jsonObjectVal = JSON(message.body)
let initialData = jsonObjectVal
In save action i have modifiedData object.
let jsonObjectModVal = JSON(message.body)
let modifiedData = jsonObjectModVal
if initialFormDataJson == jsonObjectVal {
print("json object are equal save handler")
} else {
print("json object are not equal save handler")
}
Any help much appreciated pls...
Here's an example with a random data structure on how exactly you can do it:
import Foundation
final class YourObject: Decodable, Equatable {
var field1: String
var field2: Int
var field3: [String : Double]
static func == (lhs: YourObject, rhs: YourObject) -> Bool {
lhs.field1 == rhs.field1
&& lhs.field2 == rhs.field2
&& lhs.field3 == rhs.field3
}
}
let firstJSONString = """
{
"field1":"Some string",
"field2":1,
"field3":{
"Some string":2
}
}
"""
let firstJSONData = firstJSONString.data(using: .utf8)!
let firstObject = try? JSONDecoder().decode(YourObject.self, from: firstJSONData)
let secondJSONString = """
{
"field1":"Some string",
"field2":1,
"field3":{
"Some string":2
}
}
""" // Same.
let secondJSONData = secondJSONString.data(using: .utf8)!
let secondObject = try? JSONDecoder().decode(YourObject.self, from: secondJSONData)
let thirdJSONString = """
{
"field1":"Some other string",
"field2":2,
"field3":{
"Some string":3
}
}
""" // Differs.
let thirdJSONData = thirdJSONString.data(using: .utf8)!
let thirdObject = try? JSONDecoder().decode(YourObject.self, from: thirdJSONData)
print(firstObject == secondObject) // true
print(firstObject == thirdObject) // false
Note: You mentioned that the object should be dynamic, that's why it's a class. If you needed a value object, you would be able to use struct and avoid manual implementation of the ==operator.
It's just a start of course. Having a specific JSON structure in your hands you can always search for more complicated examples, internet swarms with them.
Create a NSObject class or struct from the JSON and compare all the properties to check for equality and return true/false accordingly.
Equatable protocol will come in handy here.
class A: Equatable {
func equalTo(rhs: A) -> Bool {
// whatever equality means for two As
}
}
func ==(lhs: A, rhs: A) -> Bool {
return lhs.equalTo(rhs)
}
If you want to compare two completely arbitrary JSON objects (e.g. for unit testing), I'd suggest using the GenericJSON library. Add it to your project and/or Package.swift, and then (borrowing from #lazarevzubov's answer):
import GenericJSON
// Assume `YourObject` is `Encodable`
let testObject = YourObject(field1: "Some string", field2: 1, field3: ["Some string": 2])
let expectedData = """
{
"field1":"Some string",
"field2":1,
"field3":{
"Some string":2
}
}
""".data(using: .utf8)!
let expectedJSON = try? JSON(JSONSerialization.jsonObject(with: expectedData))
let actualJSON = try? JSON(encodable: testObject)
XCTAssertEqual(actualJSON, expectedJSON, "JSON should be equal")
A nice bonus is that you don't need to add any otherwise unnecessary Decodable or Equatable conformance to your model objects.
For Compare 2 objects use === operator
for eg.
let jsonObjectModVal = JSON(message.body)
let modifiedData = jsonObjectModVal
if initialFormDataJson === jsonObjectVal {
print("json object are equal save handler")
} else {
print("json object are not equal save handler")
}

Swift: Codable - extract a single coding key

I have the following code to extract a JSON contained within a coding key:
let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])
This successfully handles the following JSONs:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
However, fails to extract a JSON with the coding key of applmusic from the following one:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
},
"spotify":{
"differentcode":"SPOT",
"music_quality":"good",
"spotify_specific_code":"absent in apple"
},
"amazon":{
"amzncode":"SPOT",
"music_quality":"good",
"stanley":"absent in apple"
}
}
The data models for applmusic,spotify and amazon are different. However, I need only to extract applmusic and omit other coding keys.
My Swift data model is the following:
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
The API responds with the full JSON and I cannot ask it to give me only the needed fields.
How to decode only the specific part of the json? It seems, that Decodable requires me to deserialize the whole json first, so I have to know the full data model for it.
Obviously, one of the solutions would be to create a separate Response model just to contain the applmusicparameter, but it looks like a hack:
public struct Response: Codable {
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
// The only parameter is `applmusic`, ignoring the other parts - works fine
public let applmusic: Applmusic
}
Could you propose a better way to deal with such JSON structures?
A little bit more insight
I use it the following technique in the generic extension that automatically decodes the API responses for me. Therefore, I'd prefer to generalize a way for handling such cases, without the need to create a Root structure. What if the key I need is 3 layers deep in the JSON structure?
Here is the extension that does the decoding for me:
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get,
path: Path,
codingKey: String? = nil,
parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) {
if let key = codingKey {
guard let value = try decoder.decode([String:Response].self, from: $0)[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: $0)
}
}
}
The API is defined like this:
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
Updated: I made an extension of JSONDecoder out of this answer, you can check it here: https://github.com/aunnnn/NestedDecodable, it allows you to decode a nested model of any depth with a key path.
You can use it like this:
let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")
You can make a Decodable wrapper (e.g., ModelResponse here), and put all the logic to extract nested model with a key inside that:
struct DecodingHelper {
/// Dynamic key
private struct Key: CodingKey {
let stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
let intValue: Int?
init?(intValue: Int) {
return nil
}
}
/// Dummy model that handles model extracting logic from a key
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String)!
let values = try decoder.container(keyedBy: Key.self)
nested = try values.decode(NestedModel.self, forKey: key)
}
}
static func decode<T: Decodable>(modelType: T.Type, fromKey key: String) throws -> T {
// mock data, replace with network response
let path = Bundle.main.path(forResource: "test", ofType: "json")!
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let decoder = JSONDecoder()
// ***Pass in our key through `userInfo`
decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!] = key
let model = try decoder.decode(ModelResponse<T>.self, from: data).nested
return model
}
}
You can pass your desired key through userInfo of JSONDecoder ("my_model_key"). It is then converted to our dynamic Key inside ModelResponse to actually extract the model.
Then you can use it like this:
let appl = try DecodingHelper.decode(modelType: Applmusic.self, fromKey: "applmusic")
let amazon = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "amazon")
let spotify = try DecodingHelper.decode(modelType: Spotify.self, fromKey: "spotify")
print(appl, amazon, spotify)
Full code:
https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904
Bonus: Deeply nested key
After playing around more, I found you can easily decode a key of arbitrary depth with this modified ModelResponse:
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
// Split nested paths with '.'
var keyPaths = (decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String).split(separator: ".")
// Get last key to extract in the end
let lastKey = String(keyPaths.popLast()!)
// Loop getting container until reach final one
var targetContainer = try decoder.container(keyedBy: Key.self)
for k in keyPaths {
let key = Key(stringValue: String(k))!
targetContainer = try targetContainer.nestedContainer(keyedBy: Key.self, forKey: key)
}
nested = try targetContainer.decode(NestedModel.self, forKey: Key(stringValue: lastKey)!)
}
Then you can use it like this:
let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")
From this json:
{
"apple": { ... },
"amazon": {
"amzncode": "SPOT",
"music_quality": "good",
"stanley": "absent in apple"
},
"nest1": {
"nest2": {
"amzncode": "Nest works",
"music_quality": "Great",
"stanley": "Oh yes",
"nest3": {
"amzncode": "Nest works, again!!!",
"music_quality": "Great",
"stanley": "Oh yes"
}
}
}
}
Full code: https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834
You don't really need the nested struct Applmusic inside Response. This will do the job:
import Foundation
let json = """
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
"""
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
public struct Response: Codable {
public let applmusic: Applmusic
}
if let data = json.data(using: .utf8) {
let value = try! JSONDecoder().decode(Response.self, from: data).applmusic
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
Edit: Addressing your latest comment
If the JSON response would change in a way that the applmusic tag is nested, you would only need to properly change your Response type. Example:
New JSON (note that applmusic is now nested in a new responseData tag):
{
"responseData":{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
}
The only change needed would be in Response:
public struct Response: Decodable {
public let applmusic: Applmusic
enum CodingKeys: String, CodingKey {
case responseData
}
enum ApplmusicKey: String, CodingKey {
case applmusic
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let applmusicKey = try values.nestedContainer(keyedBy: ApplmusicKey.self, forKey: .responseData)
applmusic = try applmusicKey.decode(Applmusic.self, forKey: .applmusic)
}
}
The previous changes wouldn't break up any existing code, we are only fine-tuning the private implementation of how the Response parses the JSON data to correctly fetch an Applmusic object. All calls such as JSONDecoder().decode(Response.self, from: data).applmusic would remain the same.
Tip
Finally, if you want to hide the Response wrapper logic altogether, you may have one public/exposed method which will do all the work; such as:
// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
return try JSONDecoder().decode(Response.self, from: data).applmusic
}
Hiding the fact that Response even exists (make it private/inaccessible), will allow you to have all the code through your app only have to call decodeAppleMusic(data:). For example:
if let data = json.data(using: .utf8) {
let value = try! decodeAppleMusic(data: data)
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
Recommended read:
Encoding and Decoding Custom Types
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
Interesting question. I know that it was 2 weeks ago but I was wondering
how it can be solved using library KeyedCodable I created. Here is my proposition with generic:
struct Response<Type>: Codable, Keyedable where Type: Codable {
var responseObject: Type!
mutating func map(map: KeyMap) throws {
try responseObject <-> map[map.userInfo.keyPath]
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}
helper extension:
private let infoKey = CodingUserInfoKey(rawValue: "keyPath")!
extension Dictionary where Key == CodingUserInfoKey, Value == Any {
var keyPath: String {
set { self[infoKey] = newValue }
get {
guard let key = self[infoKey] as? String else { return "" }
return key
}
}
use:
let decoder = JSONDecoder()
decoder.userInfo.keyPath = "applmusic"
let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
Please notice that keyPath may be nested more deeply I mean it may be eg. "responseData.services.applemusic".
In addition Response is a Codable so you can encode it without any additional work.

Type 'customDataObject' does not conform to protocol 'Sequence'

What I'm trying to do is retrieve json data(which is in array format) and check to see if my local array already contains the data, if it does move on to the next value in the JSON data until their is a value that the array doesn't contain then append it to the array. This data in the array must be in order. I'm attempting to do this now but get the error:
Type 'ResultsGenrePosters' does not conform to protocol 'Sequence'
This is what it looks like:
public struct ResultsGenrePosters: Decodable {
public let results : [GenrePosters]?
public init?(json: JSON) {
results = "results" <~~ json
}
}
public struct GenrePosters: Decodable {
public let poster : String
public init? (json: JSON) {
guard let poster: String = "poster_path" <~~ json
else {return nil}
self.poster = poster
}
static func updateGenrePoster(genreID: NSNumber, urlExtension: String, completionHandler:#escaping (_ details: [String]) -> Void){
var posterArray: [String] = []
let nm = NetworkManager.sharedManager
nm.getJSONData(type:"genre/\(genreID)", urlExtension: urlExtension, completion: {
data in
if let jsonDictionary = nm.parseJSONData(data)
{
guard let genrePosters = ResultsGenrePosters(json: jsonDictionary)
else {
print("Error initializing object")
return
}
guard let posterString = genrePosters.results?[0].poster
else {
print("No such item")
return
}
for posterString in genrePosters {
if posterArray.contains(posterString){continue
} else { posterArray.append(posterString) } //This is where the error happens
}
}
completionHandler(posterArray)
})
}
}
Alt + click on genrePosters and what does it tell you? It should say its ResultsGenrePosters because thats what the error is saying. Now look at the type of posterArray; its an array of String, not Array ResultsGenrePosters. I think you mean to write for poster in genrePosters and have confused yourself about the types because you wrote for posterString in genrePosters.
Maybe you want to use map to transform genrePosters into a [String] ?
This transforms your posterArray, if it exists into an array containing just the poster names. If it doesn't exist you get an empty array. This only works if poster is String. If its String? you should use flatMap instead.
let posterNames = genrePosters.results?.map { $0.poster } ?? [String]()

Calling function in ViewController but parameters seems wrong

I created a custom class to save a array of Products in NSUserDefaults, but when I will test in my ViewController, I always get this using the autocomplete from Xcode 7.1
The function saveToFile or loadFromFile always shows self: DataFile as unique parameter.
This is my DataFile class
import Foundation
class DataFile {
let userDefaults = NSUserDefaults.standardUserDefaults()
func saveToFile<T>(object: [T], key: String) -> String {
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(object as! AnyObject)
self.userDefaults.setObject(encodedData, forKey: key)
self.userDefaults.synchronize()
return "oi"
}
func loadFromFile<T>(key: String) -> [T]? {
let decoded = self.userDefaults.objectForKey(key) as! NSData
if let decodedProducts = NSKeyedUnarchiver.unarchiveObjectWithData(decoded) as? [T] {
return decodedProducts
}
return nil
}
}
What am I doing wrong?
Thank you
Your funcs are not static function, so, you must create object to use these functions.
let xxx = DataFile()
xxx.loadFromFile(<#T##key: String##String#>)

Resources