SwiftUI: CoreData and complex JSON - json

I'm trying to save JSON to CoreData but the JSON is a little bit complex with many array objects. I could only save and display simple attributes like String, Int... but if I add attributes with Transformable type like [Currency], [Double], the app will crash.
Could you please show me the right way to implement this?
My Data Model
struct Currency: Decodable {
let code, name, symbol: String?
}
struct Language : Decodable {
let name : String?
}
struct WorldData: Identifiable, Decodable {
var id : Int {
return population
}
var name : String?
...
var latlng : [Double]?
var currencies : [Currency]?
var languages : [Language]
...
}
My Manual NSManagedObject subclass
extension CDCountry {
#nonobjc public class func fetchRequest() -> NSFetchRequest<CDCountry> {
return NSFetchRequest<CDCountry>(entityName: "CDCountry")
}
#NSManaged public var name: String?
#NSManaged public var latlng: NSObject?
#NSManaged public var currencies: NSObject?
#NSManaged public var languages: NSObject?
...
public var wrapperLatlng : NSObject {
latlng ?? ["Unknown error"] as NSObject
}
public var wrapperCurrencies : NSObject {
currencies ?? ["Unknown error"] as NSObject
}
...
and how I save loaded JSOn to CoreData
static func loadDataToCD(moc: NSManagedObjectContext) {
loadDataFromJSON { (countries) in
DispatchQueue.main.async {
var tempCountries = [CDCountry]()
for country in countries {
let newCountry = CDCountry(context: moc)
...
newCountry.currencies = country.currencies as NSObject?
newCountry.languages = country.languages as NSObject
newCountry.latlng = country.latlng as NSObject?
newCountry.name = country.name
...
tempCountries.append(newCountry)
}
do {
try moc.save()
} catch let error {
print("Could not save to CD: ", error.localizedDescription)
}
}

There is no easy way to answer your question but I can point you to some resources.
There is a good example on decoding JSON within the Landmark.swift file included in the "Handing User Input" Tutorial that Apple provides for SwiftUI. That might help with the basics. https://developer.apple.com/tutorials/swiftui/handling-user-input
The tutorial uses custom Coordinates and Category classes that can likely mimic the process for your Currency and Language objects. But of course that would be for a Non-Managed Object.
To do it Managed it would be best to create CoreData objects for Currency and Language and join them as a relationship in CoreData vs making them a Transformable.
Then you can follow the answer for this older question that was answered very throughly Save complex JSON to Core Data in Swift
You can also look at this question Swift And CoreData With Custom Class As Transformable Object

Related

How to convert an array of multiple object types to JSON without missing any object attribute in Swift?

In my iOS Swift project, I need to convert objects to JSON.
I have one simple class:
class Car : Encodable
{
var brand: String
init(brand: String)
{
self.brand = brand
}
}
and one subclass:
class SUVCar : Car
{
var weight: Int
init(_ weight: Int)
{
self.weight = weight
super.init(brand: "MyBrand")
}
}
I use the following generic function to convert objects and arrays to JSON:
func toJSON<T : Encodable>(_ object: T) -> String?
{
do
{
let jsonEncoder = JSONEncoder()
let jsonEncode = try jsonEncoder.encode(object)
return String(data: jsonEncode, encoding: .utf8)
}
catch
{
return nil
}
}
Now let's say I want to convert the following variable to JSON:
var arrayOfCars: Array<Car> = []
arrayOfCars.append(SUVCar(1700))
arrayOfCars.append(SUVCar(1650))
I use Array<Car> as the type for that array because there are other types of cars in that array. I just made it simpler here for the sake of readability.
So here is what I did:
let json = toJSON(arrayOfCars)
But for some reason, when converting to JSON, the weight attribute of SUVCar is ignored, even though arrayOfCars contains SUVCar objects, and I get a JSON that looks like this:
[{brand: "MyBrand"}, {brand: "MyBrand"}]
So how can I get the weight attribute of SUVCar in my JSON? What did I miss?
Thanks.
class SUVCar: Car
{
enum SUVCarKeys: CodingKey {
case weight
}
var weight: Int
init(_ weight: Int)
{
self.weight = weight
super.init(brand: "MyBrand")
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SUVCarKeys.self)
try container.encode(weight, forKey: .weight)
try super.encode(to: encoder)
}
}
If you augment the subclass's implementation of encode then you can add in additional properties
Instead of customising all your subclasses to encode properly you could solve this by introducing a type that hold your different kind of cars
struct CarCollection: Encodable {
let suvs: [SUVCar]
let jeeps: [Jeep]
let sedans: [Sedan]
}
(assuming two other sublclasses class Jeep: Car {} and class Sedan: Car {})
Then you would need no extra code and encoding would be simple
let cars = CarCollection(suvs: [SUVCar(brand: "x", weight: 1000)],
jeeps: [Jeep(brand: "y")],
sedans: [Sedan(brand: "z")])
if let json = toJSON(cars) {
print(json)
}
{"jeeps":[{"brand":"x"}],"suvs":[{"brand":"x"}],"sedans":[{"brand":"a"}]}
A bit off topic but struct might be a better choice than class here or at least it would be the recommended choice so here is how it would look with struct and a protocol instead of the superclass. The code above would still be the same.
protocol Car : Encodable {
var brand: String { get set }
}
struct SUVCar : Car {
var brand: String
var weight: Int
}
struct Jeep: Car {
var brand: String
}
struct Sedan: Car {
var brand: String
}

how to Converting JSON into Codable in swift 4.2?

I am using Xcode 10.1 and Swift 4.2. When i try to convert JSON response into Codable class it gives an error that Expected to decode Array<Any> but found a string/data instead.
My Actual JSON response is Like this from API .
{
"d": "[{\"Data\":{\"mcustomer\":[{\"slno\":1000000040.0,\"fstname\":null}]},\"Status\":true}]"
}
My Model is Like this
class MainData: Codable{
var d: [SubData]
}
class SubData : Codable {
var Data : Customer
var Status : Bool?
}
class Customer : Codable {
var mcustomer : [Detail]
}
class Detail : Codable {
var slno : Double?
var fstname : String?
}
And I am Decode this Model using JSONDecoder()
let decoder = JSONDecoder()
let deco = try decoder.decode(MainData.self, from: data)
but, I am unable to Decode this Json into My Model.
Your API is wrong. You array in json shouldn't have quotation marks around it. Otherwise you're declaring that value for key "d" is string
"[...]"
[...]
Suggestions:
Variables and constants should start with small capital letter. Otherwise for example your Data property would cause confusion with Data type. For renaming it while decoding you can use CodingKeys
If you don't need to encode your model, you can just implement Decodable protocol
You can use struct instead of class for your model
The top-level JSON object is a dictionary with the key "d" and a string value, representing another JSON object (sometimes called "nested JSON"). If the server API cannot be changed then the decoding must be done in two steps:
Decode the top-level dictionary.
Decode the JSON object from the string obtained in step one.
Together with Robert's advice about naming, CodingKeys and using structs it would look like this:
struct MainData: Codable {
let d: String
}
struct SubData : Codable {
let data : Customer
let status : Bool
enum CodingKeys: String, CodingKey {
case data = "Data"
case status = "Status"
}
}
struct Customer : Codable {
let mcustomer : [Detail]
}
struct Detail : Codable {
let slno : Double
let fstname : String?
}
do {
let mainData = try JSONDecoder().decode(MainData.self, from: data)
let subData = try JSONDecoder().decode([SubData].self, from: Data(mainData.d.utf8))
print(subData)
} catch {
print(error)
}
For your solution to work, the JSON reponse has to be following format
let json = """
{
"d": [
{
"Data": {
"mcustomer": [
{
"slno": 1000000040,
"fstname": null
}
]
},
"Status": true
}
]
}
"""
But, as you can see, the JSON response you are getting is quite different than you are expecting. Either you need to ask to change the response or you need to change your model.

How to map JSON String to model class using Object Mapper in swift

My model class is like:
class CalendarTaskModel: Mappable {
var kpiColor: String?
var kpi: String?
var date: String?
required init?(map: Map) {
//Code here
}
func mapping(map: Map) {
kpiColor <- map["kpiColor"]
kpi <- map["kpi"]
date <- map["date"]
}
}
I have an object mapped with a model class.
var taskDetails: [CalendarTaskModel]?
As my object is of array type so I Want to map JSON string to object using ObjectMapper like the code below.
code 1: taskDetails = Mapper<[CalendarTaskModel]>().map(JSONString: jsonStr)
//
code 2: taskDetails = Mapper<CalendarTaskModel>().map(JSONString: jsonStr)
but I am getting errors && Please suggest how to do this?
Thanks in advance.
I figured it out! You should use the mapArray method instead:
let jsonStr = ...
var taskDetails: [CalendarTaskModel]?
taskDetails = Mapper<CalendarTaskModel>().mapArray(JSONfile: jsonStr)
This is because the map method does not return an array.
As for the code 1 that you provided, the [CalendarTaskModel] type (equivalent to Array<CalendarTaskModel> is not compliant with that mappable protocol. I suspect it is possible to make it compliant, for instance with more complex logic, but the library encourages you to use the method I suggested. Best of luck!

How to map dynamic properties in model class (Swift)

I am new to IOS Development, i came across a really interesting situation, i have this json response coming server side
{
"caps": {
"first_key": "34w34",
"first_char": "34w45",
"first_oddo": "34w34"
.... : .....
.... : .....
}
}
My issue this that keys inside "caps" object can be dynamic (like what if one more value is added). I am using ObjectMapper to mapper to map values from response to model class. I have this model class
class User: Mappable {
var first_key: String?
var first_char: String?
var first_oddo: String?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
first_key <- map["first_key"]
first_char <- map["first_char"]
first_oddo <- map["first_oddo"]
}
}
Now as i dont know how to populate my model if values in json response are changed (because that are dynamic). I hope i have explained it well. I think i dont want hard coded values inside model?
This solution uses the Codable protocol introduced with Swift 4.
If the keys of your JSON are dynamic then you need a Dictionary.
JSON
Given this JSON
let data = """
{
"caps": {
"first_key": "34w34",
"first_char": "34w45",
"first_oddo": "34w34"
}
}
""".data(using: .utf8)!
The Response Model
You can define a struct like this
struct Response:Codable {
let caps: [String:String]
}
Decoding 🎉🎉🎉
Now you can decode your JSON
if let response = try? JSONDecoder().decode(Response.self, from: data) {
print(response.caps)
}
Output
["first_key": "34w34", "first_oddo": "34w34", "first_char": "34w45"]

Mapping a JSON object to a Swift class/struct

I need to "replicate" an entiry which is returned from a remote web API service in JSON. It looks like this:
{
"field1": "some_id",
"entity_name" = "Entity1"
"field2": "some name",
"details1": [{
"field1": 11,
"field2": "some value",
"data": {
"key1": "value1",
"key2": "value2",
"key3": "value3",
// any other, unknown at compile time keys
}
}],
"details2": {
"field1": 13,
"field2": "some value2"
}
}
Here's my attempt:
struct Entity1 {
struct Details1 {
let field1: UInt32
let field2: String
let data: [String: String]
}
struct Details2 {
let field1: UInt32
let field2: String
}
let field1: String
static let entityName = "Entity1"
let field2: String
let details1: [Details1]
let details2: Details2
}
Is it a good idea to use structs instead of classes for such a goal
as mine?
Can I anyhow define a nested struct or a class, say
Details1 and create a variable of it at the same time?
Like this:
//doesn't compile
struct Entity1 {
let details1: [Details1 {
let field1: UInt32
let field2: String
let data: [String: String]
}]
You can use any if the following good open-source libraries available to handle the mapping of JSON to Object in Swift, take a look :
Mapper
ObjectMapper
JSONHelper
Argo
Unbox
Each one have nice a good tutorial for beginners.
Regarding the theme of struct or class, you can consider the following text from The Swift Programming Language documentation:
Structure instances are always passed by value, and class
instances are always passed by reference. This means that they are
suited to different kinds of tasks. As you consider the data
constructs and functionality that you need for a project, decide
whether each data construct should be defined as a class or as a
structure.
As a general guideline, consider creating a structure when one or more
of these conditions apply:
The structure’s primary purpose is to encapsulate a few relatively simple data values.
It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an
instance of that structure.
Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
The structure does not need to inherit properties or behavior from another existing type.
Examples of good candidates for structures include:
The size of a geometric shape, perhaps encapsulating a width property and a height property, both of type Double.
A way to refer to ranges within a series, perhaps encapsulating a start property and a length property, both of type Int.
A point in a 3D coordinate system, perhaps encapsulating x, y and z properties, each of type Double.
In all other cases, define a class, and create instances of that class
to be managed and passed by reference. In practice, this means that
most custom data constructs should be classes, not structures.
I hope this help you.
HandyJSON is exactly what you need. See code example:
struct Animal: HandyJSON {
var name: String?
var id: String?
var num: Int?
}
let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"
if let animal = JSONDeserializer.deserializeFrom(json: jsonString) {
print(animal)
}
https://github.com/alibaba/handyjson
Details
Xcode 10.2.1 (10E1001), Swift 5
Links
Pods:
Alamofire - loading data
More info:
Codable
More samples of usage Codable and ObjectMapper in Swift 5
Task
Get itunes search results using iTunes Search API with simple request https://itunes.apple.com/search?term=jack+johnson
Full sample
import UIKit
import Alamofire
// Itunce api doc: https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#searching
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
private func loadData() {
let urlString = "https://itunes.apple.com/search?term=jack+johnson"
Alamofire.request(urlString).response { response in
guard let data = response.data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(ItunceItems.self, from: data)
print(result)
} catch let error {
print("\(error.localizedDescription)")
}
}
}
}
struct ItunceItems: Codable {
let resultCount: Int
let results: [ItunceItem]
}
struct ItunceItem: Codable {
var wrapperType: String?
var artistId: Int?
var trackName: String?
var trackPrice: Double?
var currency: String?
}
you could use SwiftyJson and let json = JSONValue(dataFromNetworking)
if let userName = json[0]["user"]["name"].string{
//Now you got your value
}
Take a look at this awesome library that perfectly fits your need, Argo on GitHub.
In your case, a struct is ok. You can read more on how to choose between a struct and a class here.
You can go with this extension for Alamofire https://github.com/sua8051/AlamofireMapper
Declare a class or struct:
class UserResponse: Decodable {
var page: Int!
var per_page: Int!
var total: Int!
var total_pages: Int!
var data: [User]?
}
class User: Decodable {
var id: Double!
var first_name: String!
var last_name: String!
var avatar: String!
}
Use:
import Alamofire
import AlamofireMapper
let url1 = "https://raw.githubusercontent.com/sua8051/AlamofireMapper/master/user1.json"
Alamofire.request(url1, method: .get
, parameters: nil, encoding: URLEncoding.default, headers: nil).responseObject { (response: DataResponse<UserResponse>) in
switch response.result {
case let .success(data):
dump(data)
case let .failure(error):
dump(error)
}
}