2

I'm trying the 2nd level of array in an arrayobject of a NSDictionary however it returns nil

I defined the NSDictionary at first then accessed the 1st level of array till now everything is good.

let dict = JSON(responseObject as! NSDictionary)
let arrayData = dict["data"].arrayObject! as NSArray
print("arrayData", arrayData)

When printing arrayData I get the following return

data =     (
{
categories = (
    {
    categories = "<null>";
    "category_id" = 333;
    image = "https://www.example.com/images/1.jpg";
    name = "Sub Category One";
    "parent_id" = 1000;
    },
    {
    categories = "<null>";
    "category_id" = 444;
    image = "https://www.example.com/images/2.jpg";
    name = "Sub Category Two";
    "parent_id" = 1000;
    },
    {
    categories = "<null>";
    "category_id" = 555;
    image = "https://www.example.com/images/3.jpg";
    name = "Sub Category Three";
    "parent_id" = 1000;
    }
);//end of categories array
"category_id" = 1000;
image = "https://www.examples.com/images/category.jpg";
name = "Parent Category";
"parent_id" = 0;
},
)

Now I am trying to access and print the categories from the above returned array

So I tried the following:

if dict["categories"].arrayObject != nil{
  let arrayData2 = dict["categories"].arrayObject! as NSArray
  print("arrayData2", arrayData2)
}

I also tried it as

if dict["data"]["categories"].arrayObject != nil{
  let arrayData2 = dict["data"]["categories"].arrayObject! as NSArray
  print("arrayData2", arrayData2)
}

Both ways I'm getting nil value while it should get the real values.

EDITED: Added the API call in ViewController and the dataModel structure

The callingHTTPAPI function in View Controller

func callingHttppApi(){
  DispatchQueue.main.async{
    NetworkManager.sharedInstance.showLoader()
    let sessionId = self.defaults.object(forKey:"token");
    self.categoriesTableView.isUserInteractionEnabled = false

      var requstParams = [String:Any]();
      requstParams["token"] = sessionId
      NetworkManager.sharedInstance.callingHttpRequest(params:requstParams, apiname:"api/cat", cuurentView: self){success,responseObject in
        if success == 1{
          let dict = responseObject as! NSDictionary;
          if dict.object(forKey: "fault") != nil{
            let fault = dict.object(forKey: "fault") as! Bool;
            if fault == true{
              self.loginRequest()
            }
          }else{
            NetworkManager.sharedInstance.dismissLoader()
            self.view.isUserInteractionEnabled = true
            self.categoriesTableView.isUserInteractionEnabled = true
            let dict = JSON(responseObject as! NSDictionary)
            let arrayData = dict["data"].arrayObject! as NSArray
            if dict["categories"].arrayObject != nil{
              let arrayData2 = dict["categories"].arrayObject! as NSArray
              print("arrayData2", arrayData2)
            }
            print("arrayData", arrayData)
            if dict["error"].intValue == 1{
              NetworkManager.sharedInstance.showErrorMessageWithBack(view: self, message: NetworkManager.sharedInstance.language(key: "error"))
            }else{
              self.categoriesCollModel = CategoriesViewModel(data:JSON(responseObject as! NSDictionary))
              dump (self.categoriesCollModel)
            }
          }
        }else if success == 2{
          NetworkManager.sharedInstance.dismissLoader()
          self.callingHttppApi()
        }
      }
    }
  }

The dataModel structure to cast the values

//
//  CategoriesViewModel.swift
//

import Foundation

class Categories: NSObject{
    var id:String = ""
    var name:String = ""
    var image:String = ""

    init(data:JSON){
        self.id = data["category_id"].stringValue
        self.name  = data["name"].stringValue
        self.image = data["image"].stringValue
    }

}

class SubCategories:NSObject{
    var pid:String = ""
    var id:String = ""
    var name:String = ""
    var image:String = ""

    init(data:JSON) {
        self.pid = data["parent_id"].stringValue
        self.id = data["category_id"].stringValue
        self.name  = data["name"].stringValue
        self.image = data["image"].stringValue
    }

}

struct Categories_Data{
    var id:String = ""
    var categoriesArray = [SubCategories]()

    init(data:JSON) {
        self.id = data["category_id"].stringValue
        if let arrayData = data["data"]["categories"].arrayObject {
            categoriesArray =  arrayData.map({(value) -> SubCategories in
                return  SubCategories(data:JSON(value))
            })
        }
    }

}

class CategoriesViewModel:NSObject{
    var categoryModel = [Categories]()
    var subCategories = [SubCategories]()
  var categories_Data = [Categories_Data]()

    init(data:JSON) {

        let arrayData = data["data"].arrayObject! as NSArray
        categoryModel =  arrayData.map({(value) -> Categories in
            return  Categories(data:JSON(value))
        })

        if data["data"]["categories"].arrayObject != nil{
            let arrayData2 = data["data"]["categories"].arrayObject! as NSArray
            categories_Data =  arrayData2.map({(value) -> Categories_Data in
                return  Categories_Data(data:JSON(value))
            })
        }

        //categoryModel = Categories(data:data)


    }

    var getCategories:Array<Categories>{
        return categoryModel
    }

  var getSubCategories:Array<SubCategories>{
        return subCategories
    }

}
Cœur
  • 37,241
  • 25
  • 195
  • 267
David Buik
  • 522
  • 1
  • 8
  • 31
  • what are you getting at `print("arrayData", arrayData)` n and `print("arrayData2", arrayData2)` ? – Amit Sep 18 '18 at 06:45
  • In the answer is shows the return ... please check after line **When printing arrayData I get the following return** I can't post the return here it's so long. – David Buik Sep 18 '18 at 06:47
  • 1
    Basically don't use `NSArray/NSDictionary` in Swift. You throw away the crucial type information. – vadian Sep 18 '18 at 06:48

5 Answers5

3

I highly recommend to drop SwiftyJSON and use Decodable.

The CategoriesViewModel can be reduced to

struct Root: Decodable {
    let data : [Category]
}

struct Category: Decodable {
    let categories : [Category]?
    let categoryId : Int
    let image : URL
    let name : String
    let parentId : Int
}

You need to get the raw data from the network request (in the following code named data) then you can simply decode the data into structs and get the values with dot notation.

I added a helper function to be able to print the nested categories recursively

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let result = try decoder.decode(Root.self, from: data)
    printCategories(result.data)
} catch { print(error) }

func printCategories(_ categories : [Category]) {
    for item in categories {
        if let subCategories = item.categories {
            printCategories(subCategories)
        }
        print(item.name, item.categoryId)
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
0

You are retrieve objects in wrong manner. Always retrieve object or value with check nil value...
Check below code to retrieve your categories.

let dict = JSON(responseObject as! NSDictionary)
    if let data = dict["data"] as? Array<Any>
    {
        if let categoryArray = data["categories"] as? Array<Dictionary<String,Any>>
        {
            if categoryArray.count > 0
            {
                print(categoryArray)
            }
        }
    }
Rakesh Patel
  • 1,673
  • 10
  • 27
  • I deeply appreciate your answers and support. I've edited my question and added the `callingHTTPAPI` function how I'm calling API to get JSOn response then sharing it with the dataModel. 2 things I'm facing, the datamodel is receiving only the 1st level of array and not getting the 2nd level as my main question is. And also once I call back the `CategoriesViewModel` in my `ViewController` I'm getting `nil` value not able to use the strings in a `TableView` like category name and image and id. – David Buik Sep 18 '18 at 06:31
  • Which is 2nd level array?, In your response only one array is there. – Rakesh Patel Sep 18 '18 at 06:42
  • In the response the first level is `data` which has the Parent Category and the 2nd level is `categories` which have the sub-categories if found and not `nil` – David Buik Sep 18 '18 at 06:43
0

It is looking quite complicated but it is very simple, just follow the hierarchy

lets say for example you want to reach "data -> categories -> name" as in your responce you can go like this

this is you main dictionary which contains everything in it.

if let dict = responceObject as? NSDictionary {
// now lets say you want go to your categories which is array of dictionaries

  if let arrDict = dict["categories"] as? NSArray {
      print("arrDict")

      // Now you can go anywhere in that array of dictionaries
      // For example you want to get name you can do like this

      for obj in arrDict {
         if let name = obj["name"] as? String {
            print(name)
         }
      }
  }
}

I am adding edit part here

// In your webcall lets say you receive a JSON(i am naming it as "json") object with type Any
if let jsonObj = json as? [String: Any], let mainDict = jsonObj["data"] as? [[String: Any]] {
    for arr in mainDict {
       if let arrDict = arr["categories"] as? [[String: Any]] {
           print(arrDict)
           // Now you can dig that dict which is array of dictionaries
           for obj in arrDict {
              if let name = obj["name"] as? String {
                 print(name)
              }

              if let id = obj["id"] as? Int { // Int or whatever it is
                 print(id)  
              } 
           }
       }
    }
}

Hope this will work.

Jayraj Vala
  • 224
  • 2
  • 8
0

You should also use guard to validate the values when initialising your model objects:

init(data: JSON) {
    guard
        let id = data["category_id"] as? Int,
        let name = data["name"] as? String,
        let image = data["image"] as? String
        else {
             return
        }
    self.id = id
    self.name  = name
    self.image = image
}

Note that id is an integer value, NOT a string so you should change your model.

Also check out Codable.

JonJ
  • 1,061
  • 6
  • 8
  • You mean guard will help cast the model values back to use them into the controllers? – David Buik Sep 18 '18 at 06:53
  • Guard ensures that the data you're expecting is correct and not nil. Guard will fail if the type doesn't match, e,.g. you state that you're expecting a string but an integer is being provided. – JonJ Sep 18 '18 at 07:00
  • Thanks for the explanation as mentioned this is an old project I'm updating it eventually it's based on swift 3.2 and soon I'll move to 4 and learn more about Codable – David Buik Sep 18 '18 at 07:04
  • I'd also recommend making sure your models conform to `CustomStringConvertible` and `CustomDebugStringConvertible`. You can then debug them much more easily. – JonJ Sep 18 '18 at 07:07
0

Well first understand the response of print("arrayData", arrayData). It is an array of Dictionaries.

When your adding :

let arrayData = dict["data"].arrayObject! as NSArray

you are saving the data in an array from the dictionary.

Now to fetch the categories you have to use arrayData:

let firstDict = arrayData[0] as! [String:Any]
let arrayData2 = firstDict["categories"] as! [[String:Any]]
print("arrayData2", arrayData2)

I added this code to show one data.

For more data you can use for loop:

for dictData in arrayData {
    let arrayData2 = dictData["categories"] as! [[String:Any]]
    print("arrayData2", arrayData2)
}

and using this loop you can manage or save the relevant data.

One more thing don't forget to add nil handling part.

One more thing while working on swift don't use NSArray or NSDictionary.

Try to use Codable. There are lots of examples on internet and will make your code a bit clean.

Amit
  • 4,837
  • 5
  • 31
  • 46
  • Thanks for the answer, this is a bit old project updating it using swift 3.2. I'll move to swift 4 soon and learn more about `Codable`. I tried your example it works fine however you limited the access to `arrayData[0]` I wanna keep it dynamic and access all the subcategories of each parent categories – David Buik Sep 18 '18 at 07:02
  • Updated the answer. Please check . – Amit Sep 18 '18 at 07:08
  • If I am correct this is the code structure `for dictData in arrayData { if dictData != nil{ let arrayData2 = dictData["categories"] as! [[String:Any]] print("arrayData2", arrayData2) } }` One thing i'm getting is **Type 'Any' has no subscript members** – David Buik Sep 18 '18 at 07:11
  • It's because of `Swift 3`. This URL : https://stackoverflow.com/questions/39423367/correctly-parsing-json-in-swift-3 will help you. – Amit Sep 18 '18 at 07:20