3

I have a nested dictionary and an array containing path fragment. I need to update the value at that location.

I am possibly looking for a recursive function instead of extensions to Dictionary types and such.

I am not able to do this recursively because I making a copy of the inout param.

var dict: [String: Any] = ["channel": ["item": ["title": 1111]]]
var pathFrag = ["channel", "item", "title"]
var val = 123

func addAt(pathFrag: inout [String], val: Int, data: inout [String: Any]) {
    if let first = pathFrag.first {
        if let item = data[first] {
            print(item)
            pathFrag.remove(at: 0)
            if !pathFrag.isEmpty {
                var d: [String: Any] = data[first] as! [String: Any]
                print("e: \(d)")
                return addAt(pathFrag: &pathFrag, string: string, data: &d)
            } else {
                data[first] = val
                print("else: \(data)")  // ["title": 123]
            }
        }
    }
}


addAt(pathFrag: &pathFrag, val: val, data: &dict)
print(dict)

How to update the value of title to 123?

6
  • 1
    It would be better not to use inout at all. Now you can recurse in a helper function that returns the final value. Commented Mar 16, 2019 at 16:09
  • I was a bit concerned when the dictionary is large and making copies would take a performance hit. Commented Mar 16, 2019 at 16:29
  • Understood but you can’t have your cake and eat it too. If you want this recursive algorithm, it seems to me foolish to throw away the advantage of value types that allow you to reason clearly and write just the kind of algorithm you want. And premature optimization is always, uh, premature. Commented Mar 16, 2019 at 16:30
  • Also I would point out that this entire notion is unSwifty. The use of Any is a bad smell. In Objective-C this would be one-liner thanks to KVC and keypaths. Commented Mar 16, 2019 at 16:34
  • @matt How to do it using Objective-C? I tried var d = dict as NSDictionary; d.setValue(123, forKeyPath: "channel.item.title");, but it is giving SIGABRT. Commented Mar 16, 2019 at 16:47

3 Answers 3

3

This is not what you asked, but the entire premise here is unSwifty. The use of [String:Any] is a Bad Smell. It seems more like Objective-C. And indeed this whole thing is a one-liner in Objective-C:

NSMutableDictionary * d1 = [[NSMutableDictionary alloc] initWithDictionary: @{ @"title" : @1111 }];
NSMutableDictionary * d2 = [[NSMutableDictionary alloc] initWithDictionary: @{ @"item" : d1 }];
NSMutableDictionary * d3 = [[NSMutableDictionary alloc] initWithDictionary: @{ @"channel" : d2 }];

Okay, that was just to prepare the data. Here’s the one-liner:

[d3 setValue:@123 forKeyPath:@"channel.item.title"];

But I would question the entire nested dictionaries concept; it seems to me you’ve gone down a wrong well here and you need to stand back and rethink your data structure.

Sign up to request clarification or add additional context in comments.

3 Comments

I am parsing XML constructing a dictionary out of it. I think it would be better to use class/struct to represent the data instead of dictionary then.
There are XML parsing classes. I’ve explained elsewhere how to use them. Assuming you know the expected structure, you should be using nested structs if possible.
2

Note that var d: [String: Any] = data[first] as! [String: Any] makes a copy of data[first] and not a reference to it like & and inout. So, when addAt(pathFrag: &pathFrag, string: string, data: &d) is called, only d is changed. To make this change reflect on the original data[first], you have to assign d back to data[first].

Do this:

var dict: [String: Any] = ["channel": ["item": ["title": 1111]]]
var pathFrag = ["channel", "item", "title"]

func addAt(pathFrag: inout [String], data: inout [String: Any]) {
    if let first = pathFrag.first {
        if let item = data[first] {
            pathFrag.remove(at: 0)
            if !pathFrag.isEmpty {
                var d: [String: Any] = data[first] as! [String: Any]
                addAt(pathFrag: &pathFrag, data: &d)
                data[first] = d
            } else {
                data[first] = 123
            }
        }
    }
}


addAt(pathFrag: &pathFrag, data: &dict)
print(dict)

3 Comments

Thanks! Is there a way to make this tail recursive. The assignment data[first] = d comes after the function call addAt which can lead to stackoverflow.
Yes, indeed, it can me made recursive. But I assumed it would somewhat defeat the purpose of changing data by its reference. You could do data[first] = addAt(pathFrag: &pathFrag, data: &d) in the if block, add return data[first] in the else, and return data outside of the outermost if.
var d: [String: Any] = data[first] as! [String: Any] will cause a crash if you use a new key in pathFrag. eg. ["channel", "name", "title"]
0
extension Dictionary {
    subscript(jsonDict key: Key) -> [String:Any]? {
        get {
            return self[key] as? [String:Any]
        }
        set {
            self[key] = newValue as? Value
        }
    }
}

// For your case
dict[jsonDict: "channel"]?[jsonDict: "item"]?["title"] = 123

Refer to:

https://stackoverflow.com/a/41543070/9315497

https://talk.objc.io/episodes/S01E31-mutating-untyped-dictionaries

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.