I am trying to parse a nested iterative loop in swift
I am getting the response from web service in the following format
{
"categories": [{
"name": "Default Category",
"id": "default_category",
"children": [{
"uuid": "783f491fef5041438fb7a2c3bf6a3650",
"name": "Accessories",
"children": [{
"uuid": "d21b4491ff784a9bae88de279b99fac3",
"name": "All Accessories",
"children": [{
"uuid": "2b1a23c4107844ad8a7afc1b324d0ffd",
"name": "Belts",
"children": [{
"uuid": "2b1a23c4107844ad8a7afc1b324d0ffd",
"name": "Belts",
"children": []
},
{
"uuid": "2b1a23c4107844ad8a7afc1b324d0ffd",
"name": "Belts",
"children": []
}
]
},
{
"uuid": "a1c2a64c36c2461cad3d5f850e4fd0f5",
"name": "Hats",
"children": []
},
{
"uuid": "8f26bc764b8342feaa0cb7f3b96adcae",
"name": "Scarves",
"children": []
},
{
"uuid": "aa1116d1a0254ecea836cc6b32eeb9e0",
"name": "Sunglasses",
"children": []
},
{
"uuid": "9d7033233e8f47eaa69eb1aaf2e98cdd",
"name": "Watches",
"children": []
}
]
}]
}],
"uuid": "6a23415771064e7aaad59f84f8113561"
}]
}
Inside, the categories, there is 'children' key which in turn can contain another children and so on.
I want to continuously loop inside the children key until the children key is empty and insert the last child into database.
Following is the code which i have done
for currentCategory in mainCategories {
// guard against if there are child categories
guard var children = currentCategory.children, children.count > 0 else {
// Save the context
self.coreData.saveStore()
continue
}
for thisChildCategory in children {
if thisChildCategory.children?.count > 0 {
for innerChildCategory in thisChildCategory.children! {
print("innerChildCategory name \(String(describing: innerChildCategory.name))")
}
}
if let child = thisChildCategory.children {
children = child
}
// Create new object
if let currentChildCategory = self.coreData.insertNewObject(CoreDataEntities.BijouCategories.rawValue,
keyValues: ["id" : thisChildCategory.id! as Optional<AnyObject>,
"uuid" : thisChildCategory.uuid as Optional<AnyObject>,
"name" : thisChildCategory.name! as Optional<AnyObject>,
"gender" : thisChildCategory.gender as Optional<AnyObject>!,
"active" : NSNumber(value: false)]) as? BijouCategories {
// Set as parent category
currentChildCategory.parentCategory = parentCategory
// Save the context
self.coreData.saveStore()
}
}
}
But this is not saving all the last child category in database.
Swift 4
You should let Swift 4's codable do the work for you.
You can use the following class as a struct but I find using a class is better if you plan on editing the data.
class Categories: Codable {
var categories: [CategoryItems]
}
class CategoryItems: Codable {
var name: String?
var id: String?
var uuid: String?
var children: [CategoryItems]?
required init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: CodingKeys.name)
id = try container.decodeIfPresent(String.self, forKey: CodingKeys.id)
uuid = try container.decodeIfPresent(String.self, forKey: CodingKeys.uuid)
children = try container.decodeIfPresent([CategoryItems].self, forKey: CodingKeys.children)
if children != nil, children!.count == 0 {
children = nil
}
}
You can see here we add create the root level class "Categories" that has an array of CategoryItems. CategoryItems has all the possible values within it, but each item in the array may or may not have all of the possible values, hence they are optional. The important one is the children which is optional. Then in the required init we only se the optional values if the key value pair is available when decoding. I also set the children to nil if there are zero items, this is optional but helps when doing if statements later.
Then to decode your json using these codable classes you use the following code.
func decode(jsonData data: Data) {
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(Categories.self, from: data)
}
catch let error as NSError {
print("JSON Decode error = ", error)
}
}
If you want to do a quick test to see if you got the deeping children level which I did you can simply run the following on the decoded variable.
for i in decoded.categories.first!.children!.first!.children!.first!.children!.first!.children! {
print(i.name)
print(i.uuid)
}
With more than 2 nested levels a recursive function is recommended. recursive means the function calls itself.
Here is an simple example assuming jsonString is the given JSON in the question.
The function parseCategory passes the children array and the UUID string as parent identifier. The print line is the place to save the object in Core Data and of course you can pass the created Core Data object as parent as well to set the relationship.
func parseCategory(children: [[String:Any]], parent: String) {
for child in children {
print("Save in Core Data", child["name"] as! String, parent)
let descendants = child["children"] as! [[String:Any]]
parseCategory(children:descendants, parent: child["uuid"] as! String)
}
}
let data = Data(jsonString.utf8)
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
parseCategory(children: json["categories"] as! [[String:Any]], parent: "")
} catch { print(error)}
The output is
"Save in Core Data Default Category
Save in Core Data Accessories 6a23415771064e7aaad59f84f8113561
Save in Core Data All Accessories 783f491fef5041438fb7a2c3bf6a3650
Save in Core Data Belts d21b4491ff784a9bae88de279b99fac3
Save in Core Data Belts 2b1a23c4107844ad8a7afc1b324d0ffd
Save in Core Data Belts 2b1a23c4107844ad8a7afc1b324d0ffd
Save in Core Data Hats d21b4491ff784a9bae88de279b99fac3
Save in Core Data Scarves d21b4491ff784a9bae88de279b99fac3
Save in Core Data Sunglasses d21b4491ff784a9bae88de279b99fac3
Save in Core Data Watches d21b4491ff784a9bae88de279b99fac3"
Created an model class to hold your nested children in the form of a tree.
class Children {
var uuid: String?
var name: String?
var children: [Children] = [Children(array: [])]
init(array: NSArray) {
let childrenDic = array[0] as! NSDictionary
uuid = childrenDic["uuid"] as? String
name = childrenDic["name"] as? String
children[0] = Children.init(array: childrenDic["children"] as! NSArray)
}
}
Use like
var childrenModel = Children.init(array: yourArray)
I would suggest you to use ObjectMapper instead of unwrapping the json manually.
https://github.com/Hearst-DD/ObjectMapper
then everything should be much cleaner
class Child: Mappable {
var uuid: String?
var name: String?
var childern: [Child]?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
uuid <- map["uuid"]
name <- map["name"]
childern <- map["childern"]
}
}
class Category: Mappable {
var _id: String? //id is the reserved word
var name: String?
var childern: [Child]?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
_id <- map["id"]
name <- map["name"]
childern <- map["childern"]
}
}
Related
I have a model called Track. It has a set of basic and a set of extended properties. List of tracks and their basic properties are fetched with a search API call, then I need to make another API call with those track IDs to fetch their extended properties.
The question is how to best combine the results of both API calls and populate the extended properties into the already created Track objects, and of course match them by ID (which unfortunately is a different property name in both calls' results). Note that there are many more keys returned in the real results sets - around 20-30 properties for each of the two calls.
Track.swift
struct Track: Decodable {
// MARK: - Basic properties
let id: Int
let title: String
// MARK: - Extended properties
let playbackURL: String
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case playbackUrl = "playbackUrl"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let idString = try container.decode(String.self, forKey: CodingKeys.id)
id = idString.int ?? 0
title = try container.decode(String.self, forKey: CodingKeys.title)
playbackURL = try container.decodeIfPresent(String.self, forKey: CodingKeys.playbackUrl) ?? ""
}
}
ViewModel.swift
let disposeBag = DisposeBag()
var searchText = BehaviorRelay(value: "")
private let provider = MoyaProvider<MyAPI>()
let jsonResponseKeyPath = "results"
public lazy var data: Driver<[Track]> = getData()
private func searchTracks(query: String) -> Observable<[Track]> {
let decoder = JSONDecoder()
return provider.rx.request(.search(query: query))
.filterSuccessfulStatusCodes()
.map([Track].self, atKeyPath: jsonResponseKeyPath, using: decoder, failsOnEmptyData: false)
.asObservable()
}
private func getTracksMetadata(tracks: Array<Track>) -> Observable<[Track]> {
let trackIds: String = tracks.map( { $0.id.description } ).joined(separator: ",")
let decoder = JSONDecoder()
return provider.rx.request(.getTracksMetadata(trackIds: trackIds))
.filterSuccessfulStatusCodes()
.map({ result -> [Track] in
})
.asObservable()
}
private func getData() -> Driver<[Track]> {
return self.searchText.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest(searchTracks)
.flatMapLatest(getTracksMetadata)
.asDriver(onErrorJustReturn: [])
}
The JSON result for .search API call is structured like this:
{
"results": [
{
"id": "4912",
"trackid": 4912,
"artistid": 1,
"title": "Hello babe",
"artistname": "Some artist name",
"albumtitle": "The Best Of 1990-2000",
"duration": 113
},
{
...
}
]
}
The JSON result for .getTracksMetadata API call is structured like this:
[
{
"TrackID": "4912",
"Title": "Hello babe",
"Album": "The Best Of 1990-2000",
"Artists": [
{
"ArtistID": "1",
"ArtistName": "Some artist name"
}
],
"SomeOtherImportantMetadata1": "Something something 1",
"SomeOtherImportantMetadata2": "Something something 2",
"SomeOtherImportantMetadata3": "Something something 3"
},
{
...
}
]
The solution here is a two phase approach. First you should define two different structs for the two network calls and a third struct for the combined result. Let's say you go with:
struct TrackBasic {
let id: Int
let title: String
}
struct TrackMetadata {
let id: Int // or whatever it's called.
let playbackURL: String
}
struct Track {
let id: Int
let title: String
let playbackURL: String
}
And define your functions like so:
func searchTracks(query: String) -> Observable<[TrackBasic]>
func getTracksMetadata(tracks: [Int]) -> Observable<[TrackMetadata]>
Now you can make the two calls and wrap the data from the two separate endpoints into the combined struct:
searchText
.flatMapLatest { searchTracks(query: $0) }
.flatMapLatest { basicTracks in
Observable.combineLatest(Observable.just(basicTracks), getTracksMetadata(tracks: basicTracks.map { $0.id }))
}
.map { zip($0.0, $0.1) }
.map { $0.map { Track(id: $0.0.id, title: $0.0.title, playbackURL: $0.1.playbackURL) } }
The above assumes that the track metadata comes in the same order that it was requested in. If that is not the case then the last map will have to be more complex.
I have used coredata long back. But, I know basics of coredata for storing data and fetching.
But, Presently I am working with Swift language.
I have local json file and I am doing parsing that by decoder and displaying that data in tableview.
let path = Bundle.main.path(forResource: "file", ofType: "json")
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path ?? ""), options: .mappedIfSafe)
let decoder = JSONDecoder()
do {
quData = try decoder.decode(quData.self, from: data)
DispatchQueue.main.async {
self.myTableView.reloadData()
}
} catch {
print("Json decoder error")
}
} catch {
print(LocalizedError.self)
}
For that I have created model class based on the key values of json.
But, Now I have to store that data to Coredata and Fetch back, Need to show in same tableview.
But, I am getting confusion how many key values should I need to create.
My model class is :
class QuData: Codable {
let qu: Qu
init(qu: Qu) {
self.qu = qu
}
}
class Qu: Codable {
let music: Music
let dance: dance
init(music: Music, dance: dance) {
self.music = music
self.dance = dance
}
}
class Maths: Codable {
let q1, q2: Q1
init(q1: Q1, q2: Q1) {
self.q1 = q1
self.q2 = q2
}
}
class Q1: Codable {
let query: String
let options: [String]
let answer: String
let q1Optional: Bool
enum CodingKeys: String, CodingKey {
case query, options, answer
case q1Optional = "optional"
}
init(question: String, options: [String], answer: String, q1Optional: Bool) {
self.query = query
self.options = options
self.answer = answer
self.q1Optional = q1Optional
}
}
class Sport: Codable {
let q1: Q1
init(q1: Q1) {
self.q1 = q1
}
}
And my JSON data is
{
"qu": {
"music": {
"q1": {
“query”: “What is your name?”,
"options": [
“Sony”,
“Samsung”,
“Apple”,
“MI”
],
"answer": “Apple”,
"optional": true
}
},
“dance”: {
"q1": {
"question": "5 + 1 = ?",
"options": [
“8”,
“9”,
“6”,
“23”
],
"answer": “23”,
"optional": false
},
"q2": {
"question": "12 - 4 = ?",
"options": [
“5”,
“4”,
“9”,
“6”
],
"answer": "4",
"optional": false
}
}
}
}
How to store these data to Coredata and fetching, Showing in tableview..
And, The two categories (music,dance) in json data, I have to show "Music" data in 1st section and "Dance" data in
section tableview.
I am fully struck, how to create this kind json structure in Entity with attributes and fetching them using same model class (Which already created for local json file parsing).
Can anyone suggest me to move on further?
My suggestion is to use one entity.
In Core Data you can filter records very efficiently, so add a type attribute representing music, dance etc. You can even add a computed property to map the type attribute to an enum. The options attribute is declared as flat string. Use another computed property to map the flat string to an array and vice versa.
class Question : NSManagedObject {
#NSManaged var type: String
#NSManaged var question: String
#NSManaged var answer: String
#NSManaged var options: String
#NSManaged var optional: Bool
enum QuestionType : String {
case music, dance
}
var questionType : QuestionType {
get { return QuestionType(rawValue: type)! }
set { type = newValue.rawValue }
}
var questionOptions : [String] {
get { return options.components(separatedBy: ", ") }
set { options = newValue.joined(separator: ", ") }
}
Alternatively use one entity per type and relationships for the questions
class Music : NSManagedObject {
#NSManaged var questions: Set<Question>
...
}
class Question : NSManagedObject {
#NSManaged var question: String
#NSManaged var answer: String
#NSManaged var options: String
#NSManaged var optional: Bool
#NSManaged var type: Music
...
}
I have a (annoying) situation where my back-end returns an object like this:
{
"user": {
"name": [
"John"
],
"familyName": [
"Johnson"
]
}
}
where each property is an array that holds a string as its first element. In my data model struct I could declare each property as an array but that really would be ugly. I would like to have my model as such:
struct User: Codable {
var user: String
var familyName: String
}
But this of course would fail the encoding/decoding as the types don't match. Until now I've used ObjectMapper library which provided a Map object and currentValue property, with that I could declare my properties as String type and in my model init method assig each value through this function:
extension Map {
public func firstFromArray<T>(key: String) -> T? {
if let array = self[key].currentValue as? [T] {
return array.first
}
return self[key].currentValue as? T
}
}
But now that I am converting to Codable approach, I don't know how to do such mapping. Any ideas?
You can override init(from decoder: Decoder):
let json = """
{
"user": {
"name": [
"John"
],
"familyName": [
"Johnson"
]
}
}
"""
struct User: Codable {
var name: String
var familyName: String
init(from decoder: Decoder) throws {
let container:KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self)
let nameArray = try container.decode([String].self, forKey: .name)
let familyNameArray = try container.decode([String].self, forKey: .familyName)
self.name = nameArray.first!
self.familyName = familyNameArray.first!
}
enum CodingKeys: String, CodingKey {
case name
case familyName
}
}
let data = json.data(using: .utf8)!
let decodedDictionary = try JSONDecoder().decode(Dictionary<String, User>.self, from: data)
print(decodedDictionary) // ["user": __lldb_expr_48.User(name: "John", familyName: "Johnson")]
let encodedData = try JSONEncoder().encode(decodedDictionary["user"]!)
let encodedStr = String(data: encodedData, encoding: .utf8)
print(encodedStr!) // {"name":"John","familyName":"Johnson"}
My tendency would be to adapt your model to the data coming in and create computed properties for use in the application, e.g.
struct User: Codable {
var user: [String]
var familyName: [String]
var userFirstName: String? {
return user.first
}
var userFamilyName: String? {
return familyName.first
}
}
This allows you to easily maintain parody with the data structure coming in without the maintenance cost of overriding the coding/decoding.
If it goes well with your design, you could also have a UI wrapper Type or ViewModel to more clearly differentiate the underlying Model from it's display.
I'm currently using AlamofireObjectMapper to create JSON objects. I'm having a tough time accessing JSON arrays:
This is the JSON Array:
[
{
"city": "string",
"country": "string",
"county": "string",
}
]
This is the function:
func getMemberDetails ( id: String) {
Alamofire.request(.GET, "\(baseURL)/api/Members/\(id)/memberDetails").responseArray { (response: Response<[MemberDetailInfo], NSError>) in
let memberDetailArray = response.result.value
if let memberDetailArray = memberDetailArray {
for memberDetail in memberDetailArray {
print(memberDetail.createDate)
print(memberDetail.id)
}
}
}
}
This Is the class:
class MemberDetailInfo: Mappable{
var city: String?
var country: String?
var county: String?
required init?(_ map: Map) {
mapping(map)
}
unc mapping(map: Map) {
city <- map["city"]
country <- map["country"]
county <- map["county"]
}
}
Whenever I step through it it just jumps right to the end, I’m not sure why it isn’t working. If anyone knows how to extract the JSON data from the array it would be greatly appreciated.
Bellow i have mentioned my model object.
class HVConnection: NSObject {
//private var _data: NSMutableDictionary
private var _data: NSMutableDictionary
// MARK:- Init
init(data: NSDictionary)
{
_data = NSMutableDictionary(dictionary: data)
}
// MARK:- Properties
var last_name: String? {
if let lastNameObject = _data.objectForKey("last_name") {
return (idObject as! String)
} else {
return nil
}
}
}
then i implemented a test case to check variables.
Bellow i have mentioned the test case.
func testNetworkModelObject() {
let connectionObject = ["network": ["first_name": "Dimuth", "last_name": "Lasantha", "business_email": "example#gmail.com", "currency": "USD", "language": "en-us", "category": "individual"]]
let modelObject = HVConnection(data: connectionObject)
XCTAssertEqual(modelObject.last_name, "Lasantha")
}
bellow i have mentioned the error
XCTAssertEqual failed: ("nil") is not equal to ("Optional("Lasantha")")
please help me to fix the issue
Your problem is that you cannot use
_data.objectForKey("last_name")
in your HVConnection because it is nested within another dictionary key called network.
So instead use:
// MARK:- Properties
var last_name: String? {
if let lastNameObject = _data.objectForKey("network")?.objectForKey("last_name") {
return (lastNameObject as! String)
} else {
return nil
}
}
This is to demonstrate the dictionaries in use:
["network": // your dictionary is a dictionary within a dictionary
["first_name": "Dimuth",
"last_name": "Lasantha",
"business_email": "example#gmail.com",
"currency": "USD",
"language": "en-us",
"category": "individual"]
]
In order to get your last_name, you have to get the dictionary for key network to get the actual dictionary, and once you have that, you can check that dictionary you get back from the network key for the key last_name.