How to access deeply nested dictionaries in Swift

后端 未结 10 869
名媛妹妹
名媛妹妹 2020-11-28 04:46

I have a pretty complex data structure in my app, which I need to manipulate. I am trying to keep track of how many types of bugs a player has in thier garden. There are te

相关标签:
10条回答
  • 2020-11-28 04:52

    If it's only about retrieval (not manipulation) then here's a Dictionary extension for Swift 3 (code ready for pasting into Xcode playground) :

    //extension
    extension Dictionary where Key: Hashable, Value: Any {
        func getValue(forKeyPath components : Array<Any>) -> Any? {
            var comps = components;
            let key = comps.remove(at: 0)
            if let k = key as? Key {
                if(comps.count == 0) {
                    return self[k]
                }
                if let v = self[k] as? Dictionary<AnyHashable,Any> {
                    return v.getValue(forKeyPath : comps)
                }
            }
            return nil
        }
    }
    
    //read json
    let json = "{\"a\":{\"b\":\"bla\"},\"val\":10}" //
    if let parsed = try JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<AnyHashable,Any>
    {
        parsed.getValue(forKeyPath: ["a","b"]) //-> "bla"
        parsed.getValue(forKeyPath: ["val"]) //-> 10
    }
    
    //dictionary with different key types
    let test : Dictionary<AnyHashable,Any> = ["a" : ["b" : ["c" : "bla"]], 0 : [ 1 : [ 2 : "bla"]], "four" : [ 5 : "bla"]]
    test.getValue(forKeyPath: ["a","b","c"]) //-> "bla"
    test.getValue(forKeyPath: ["a","b"]) //-> ["c": "bla"]
    test.getValue(forKeyPath: [0,1,2]) //-> "bla"
    test.getValue(forKeyPath: ["four",5]) //-> "bla"
    test.getValue(forKeyPath: ["a","b","d"]) //-> nil
    
    //dictionary with strings as keys
    let test2 = ["one" : [ "two" : "three"]]
    test2.getValue(forKeyPath: ["one","two"]) //-> "three"
    
    0 讨论(0)
  • 2020-11-28 04:52

    You can use this extension:

    extension Dictionary {
    
    /// - Description
    ///   - The function will return a value on given keypath
    ///   - if Dictionary is ["team": ["name": "KNR"]]  the to fetch team name pass keypath: team.name
    ///   - If you will pass "team" in keypath it will return  team object
    /// - Parameter keyPath: keys joined using '.'  such as "key1.key2.key3"
    func valueForKeyPath <T> (_ keyPath: String) -> T? {
        let array = keyPath.components(separatedBy: ".")
        return value(array, self) as? T
    
    }
    
    /// - Description:"
    ///   - The function will return a value on given keypath. It keep calling recursively until reach to the keypath. Here are few sample:
    ///   - if Dictionary is ["team": ["name": "KNR"]]  the to fetch team name pass keypath: team.name
    ///   - If you will pass "team" in keypath it will return  team object
    /// - Parameters:
    ///   - keys: array of keys in a keypath
    ///   - dictionary: The dictionary in which value need to find
    private func value(_ keys: [String], _ dictionary: Any?) -> Any? {
        guard let dictionary = dictionary as? [String: Any],  !keys.isEmpty else {
            return nil
        }
        if keys.count == 1 {
            return dictionary[keys[0]]
        }
        return value(Array(keys.suffix(keys.count - 1)), dictionary[keys[0]])
    }
    

    }

    Usage:

    let dictionary = ["values" : ["intValue": 3]]
    let value: Int = dictionary.valueForKeyPath("values.intValue")
    
    0 讨论(0)
  • 2020-11-28 04:56

    Unfortunately none of these methods worked for me, so I built my own to use a simple string path like "element0.element1.element256.element1", etc. Hope this save a time for others. (just use a dots between name of elements in string)

    Json example:

    {
        "control": {
            "type": "Button",
            "name": "Save",
            "ui": {
                "scale": 0.5,
                "padding": {
                    "top": 24,
                    "bottom": 32
                }
            }
        }
    }
    

    Step 1, convert json String to Dictionary

    static func convertToDictionary(text: String) -> [String: Any]? {
            if let data = text.data(using: .utf8) {
                do {
                    return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
                } catch {
                    print(error.localizedDescription)
                }
            }
            return nil
        }
    

    Step 2, helper to get a nested objects

    //path example: "control.ui.scale"
        static func getDictValue(dict:[String: Any], path:String)->Any?{
            let arr = path.components(separatedBy: ".")
            if(arr.count == 1){
                return dict[String(arr[0])]
            }
            else if (arr.count > 1){
                let p = arr[1...arr.count-1].joined(separator: ".")
                let d = dict[String(arr[0])] as? [String: Any]
                if (d != nil){
                    return getDictValue(dict:d!, path:p)
                }
            }
            return nil
        }
    

    Step 3, use helper

    let controlScale = getDictValue(dict:dict, path: "control.ui.scale") as! Double?
    print(controlScale)
    
    let controlName = getDictValue(dict:dict, path: "control.name") as! String?
    print(controlName)
    

    Returns

    0.5
    Save
    
    0 讨论(0)
  • 2020-11-28 04:56

    You can use the following syntax on Swift 3/4:

    if let name = data["name"] as? String {
        // name has "John"
    }
    
    if let age = data["age"] as? Int {
        // age has 30
    }
    
    if let car = data["cars"] as? [String:AnyObject],
        let car1 = car["car1"] as? String {
        // car1 has "Ford"
    }
    
    0 讨论(0)
  • 2020-11-28 04:58

    Yet another approach using various overloaded Dictionary subscript implementations:

    let dict = makeDictionary(fromJSONString:
            """
            {
                "control": {
                    "type": "Button",
                    "name": "Save",
                    "ui": {
                        "scale": 0.5,
                        "padding": {
                            "top": 24,
                            "bottom": 32
                        }
                    }
                }
            }
            """)!
    
    dict[Int.self, ["control", "ui", "padding", "top"]] // 1
    dict[Int.self, "control", "ui", "padding", "top"]   // 2
    dict[Int.self, "control.ui.padding.top"]        // 3
    

    And the actual implementations:

    extension Dictionary {
        // 1    
        subscript<T>(_ type: T.Type, _ pathKeys: [Key]) -> T? {
            precondition(pathKeys.count > 0)
    
            if pathKeys.count == 1 {
                return self[pathKeys[0]] as? T
            }
    
        // Drill down to the innermost dictionary accessible through next-to-last key
            var dict: [Key: Value]? = self
            for currentKey in pathKeys.dropLast() {
                dict = dict?[currentKey] as? [Key: Value]
                if dict == nil {
                    return nil
                }
            }
    
            return dict?[pathKeys.last!] as? T
        }
    
        // 2. Calls 1
        subscript<T>(_ type: T.Type, _ pathKeys: Key...) -> T? {
            return self[type, pathKeys]
        }
    }
    
    extension Dictionary where Key == String {
        // 3. Calls 1
        subscript<T>(_ type: T.Type, _ keyPath: String) -> T? {
            return self[type, keyPath.components(separatedBy: ".")]
        }
    }
    
    func makeDictionary(fromJSONString jsonString: String) -> [String: Any]? {
        guard let data = jsonString.data(using: .utf8)
            else { return nil}
        let ret = try? JSONSerialization.jsonObject(with: data, options: [])
        return ret as? [String: Any]
    }
    
    0 讨论(0)
  • 2020-11-28 04:59

    When working with dictionaries you have to remember that a key might not exist in the dictionary. For this reason, dictionaries always return optionals. So each time you access the dictionary by key you have to unwrap at each level as follows:

    bugsDict["ladybug"]!["spotted"]!["red"]!++
    

    I presume you know about optionals, but just to be clear, use the exclamation mark if you are 100% sure the key exists in the dictionary, otherwise it's better to use the question mark:

    bugsDict["ladybug"]?["spotted"]?["red"]?++
    

    Addendum: This is the code I used for testing in playground:

    var colorsDict = [String : Int]()
    var patternsDict =  [String : [String : Int]] ()
    var bugsDict = [String : [String : [String : Int]]] ()
    
    colorsDict["red"] = 1
    patternsDict["spotted"] = colorsDict
    bugsDict["ladybug"] = patternsDict
    
    
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 1
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 2
    bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 3
    bugsDict["ladybug"]!["spotted"]!["red"]! // Prints 4
    
    0 讨论(0)
提交回复
热议问题