David Seek David Seek - 1 month ago 17
Swift Question

Swift 3 / How to handle nested NSDictionaries since "Value of type Any has no member value"

Please dont' mark as duplicate of "Value of type Any has no member value". I'm not asking how to solve the compiler error.

My question is: What is the best practise to handle nested Dictionaries with the new need to downcast everything?

My code in Swift 2 was:

if result != nil {

let token = String((result.value(forKey: "credentials")?.value(forKey: "token"))!)
let uid = String((result.value(forKey: "uid"))!)
let bio = String((result.value(forKey: "extra")?.value(forKey: "raw_info")?.value(forKey: "data")?.value(forKey: "bio"))!)
let followed_by = String((result.value(forKey: "extra")?.value(forKey: "raw_info")?.value(forKey: "data")?.value(forKey: "counts")?.value(forKey: "followed_by"))!)
let follows = String((result.value(forKey: "extra")?.value(forKey: "raw_info")?.value(forKey: "data")?.value(forKey: "counts")?.value(forKey: "follows"))!)
let media = String((result.value(forKey: "extra")?.value(forKey: "raw_info")?.value(forKey: "data")?.value(forKey: "counts")?.value(forKey: "media"))!)
let username = String((result.value(forKey: "user_info")?.value(forKey: "username"))!)
let image = String((result.value(forKey: "user_info")?.value(forKey: "image"))!)

self.saveAccount(token, uid: uid, bio: bio, followed_by: followed_by, follows: follows, media: media, username: username, image: image)

}


I know that the error appears because I need to downcast since Swift 3.

But since I'm querying a nested Dictionary, every single step needs to be downcasted as
AnyObject
and within 10 lines of code I cast
as AnyObject
like 50 times. And some lines are like 1 Billion characters long...

if result != nil {

let token = String(describing: (((result as! NSDictionary).value(forKey: "credentials") as AnyObject).value(forKey: "token"))!)
let uid = String(describing: ((result as! NSDictionary).value(forKey: "uid"))!)
let bio = String(describing: (((((result as! NSDictionary).value(forKey: "extra") as AnyObject).value(forKey: "raw_info") as AnyObject).value(forKey: "data") as AnyObject).value(forKey: "bio"))!)
let followed_by = String(describing: ((((((result as! NSDictionary).value(forKey: "extra") as AnyObject).value(forKey: "raw_info") as AnyObject).value(forKey: "data") as AnyObject).value(forKey: "counts") as AnyObject).value(forKey: "followed_by"))!)
let follows = String(describing: ((((((result as! NSDictionary).value(forKey: "extra") as AnyObject).value(forKey: "raw_info") as AnyObject).value(forKey: "data") as AnyObject).value(forKey: "counts") as AnyObject).value(forKey: "follows"))!)
let media = String(describing: ((((((result as! NSDictionary).value(forKey: "extra") as AnyObject).value(forKey: "raw_info") as AnyObject).value(forKey: "data") as AnyObject).value(forKey: "counts") as AnyObject).value(forKey: "media"))!)
let username = String(describing: (((result as! NSDictionary).value(forKey: "user_info") as AnyObject).value(forKey: "username"))!)
let image = String(describing: (((result as! NSDictionary).value(forKey: "user_info") as AnyObject).value(forKey: "image"))!)

self.saveAccount(token, uid: uid, bio: bio, followed_by: followed_by, follows: follows, media: media, username: username, image: image)

}


For example
let followed_by


let followed_by =
String(describing: ((((((result as! NSDictionary)
.value(forKey: "extra") as AnyObject)
.value(forKey: "raw_info") as AnyObject)
.value(forKey: "data") as AnyObject)
.value(forKey: "counts") as AnyObject)
.value(forKey: "followed_by"))!)


I have another function where I query for 25+ objects and downcast like 100 times....

I know how to get the result, but is there a more advanced way to handle this scenario? Or at least to have it appear more readable? Help is very appreciated.

PS: It doesn't makes any difference if downcasted as
NSDictionary
or
AnyObject
, but since I know what I'm downcasting, I prefer to cast it as
NSDictionary
over
AnyObject
.

Answer

First of all and second Do not: Do not use NSDictionary in Swift. You throw away the type information.

And yes, you have to downcast the intermediate objects but always to something the compiler can safely work with (Swift Dictionary or Array).

Since all intermediate objects are obviously dictionaries cast them to [String:Any].

Here is a short example. For clarity a type alias for [String:Any] is used.

typealias JSONDictionary = [String:Any]

This extracts the objects in the extra node

if let result = result as? JSONDictionary  {

  let token = (result["credentials"] as! JSONDictionary)["token"] as! String
  let uid = result["uid"] as! String

  if let extra = result["extra"] as? JSONDictionary,
    let rawInfo = extra ["raw_info"] as? JSONDictionary,
    let data = rawInfo["data"] as? JSONDictionary {

    let bio = data["bio"] as! String

    if let counts = data["counts"] as! JSONDictionary{
      let followed_by = counts["followed_by"] as! String
      let follows = counts["follows"] as! String
      let media = counts["media"] as! String 
    }
  }
}

Of course this code is not tested but you get an impression how to parse nested dictionaries. If result is deserialized JSON consider to use a library like SwiftyJSON.