4

I want to safely search through values in a swift Dictionary using if lets and making sure it is type safe as I get deeper and deeper into the dictionary. The dictionary contains dictionaries that contains NSArray that contains more dictionary.

At first attempt my code looks like this:

  if let kkbox = ticket["KKBOX"] as? Dictionary<String, AnyObject> {
        if let kkboxDlUrlDict = kkbox["kkbox_dl_url_list"] as? Dictionary<String, AnyObject> {
            if let kkboxDlUrlArray = kkboxDlUrlDict["kkbox_dl_url"] as? NSArray {
                for dict in kkboxDlUrlArray {
                    if let name = dict["name"] as? String {
                        if name == mediaType.rawValue {
                            urlStr = dict["url"] as String
                        }
                    }
                }
            } else { return nil }
        } else { return nil }
    } else { return nil }

How do I shorten it to perhaps one or 2 line?

I realised I can chain it if it is 2 layers. This works:

 if let kkboxDlUrlArray = ticket["KKBOX"]?["kkbox_dl_url_list"] as? NSArray {

    }

But any chain longer than that, will not compile.

Is there a way to chain through a dictionary more than once?

Thank you

Jacky Wang
  • 618
  • 7
  • 27
  • 2
    SwiftyJSON might help simplify somewhat. See https://github.com/SwiftyJSON/SwiftyJSON. – Max MacLeod Jan 16 '15 at 11:24
  • Your `else` clauses return `nil` but your main clause assigns to `urlStr`? That won't compile. Said another way, if we know the expected return we might be able to exploit `map()` and related functions. – GoZoner Jan 16 '15 at 22:46
  • 1
    @MaxMacLeod SwiftJSON is beautiful! That's exactly what I was looking for. Much appreciation sir. – Jacky Wang Jan 19 '15 at 03:42

3 Answers3

1

You can chain, but with proper downcast at each step:

if let kkboxDlUrlArray = ((((ticket["KKBOX"] as? Dictionary<String, AnyObject>)?["kkbox_dl_url_list"]) as? Dictionary<String, AnyObject>)?["kkbox_dl_url"]) as? NSArray {
    for dict in kkboxDlUrlArray {
        println(dict)
    }
}

That doesn't look good though - it's one line, but not readable.

Personally, without using any fancy functional way to do it, I would make the chain more explicit with just one optional binding:

let kkbox = ticket["KKBOX"] as? Dictionary<String, AnyObject>
let kkboxDlUrlDict = kkbox?["kkbox_dl_url_list"] as? Dictionary<String, AnyObject>
if let kkboxDlUrlArray = kkboxDlUrlDict?["kkbox_dl_url"] as? NSArray {
    for dict in kkboxDlUrlArray {
        println(dict)
    }
}

In my opinion much easier to read, and with no unneeded indentation

Antonio
  • 71,651
  • 11
  • 148
  • 165
1

Alternatively, you could use this extension, which mimics the original valueForKeyPath method that used to exist in NSDictionary but got axed for whatever reason:

Swift 4.1

extension Dictionary where Key: ExpressibleByStringLiteral {

func valueFor(keyPath: String) -> Any? {
    var keys = keyPath.components(separatedBy: ".")
    var val : Any = self
    while keys.count > 0 {
        if let key = keys[0] as? Key {
            keys.remove(at: 0)
            if let dic = val as? Dictionary<Key, Value> {
                if let leaf = dic[key] {
                    val = leaf
                } else {
                    return nil
                }
            } else {
                return nil
            }
        } else {
            return nil
        }
    }
    return val
}

Your code would then read:

if let array = ticket.valueFor("KKBOX.kkbox_dl_url_list.kkbox_dl_url") as? [] {
  // do something with array
}
Guy Moreillon
  • 993
  • 10
  • 28
0

Seems like swift 1.2 has added this feature.

"More powerful optional unwrapping with if let — The if let construct can now unwrap multiple optionals at once, as well as include intervening boolean conditions. This lets you express conditional control flow without unnecessary nesting."

https://developer.apple.com/swift/blog/

Jacky Wang
  • 618
  • 7
  • 27