This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

Mutating a nested untyped dictionary can be a challenge. To solve it we discuss the mutability of value types and the concept of l-values.

00:06 Today let's have a look at mutating untyped dictionaries. This is a question we found on Stack Overflow, and the solution turned out to be quite complex, but very interesting. You wouldn't usually solve the problem this way, but there are some interesting lessons to be learned : how Swift works with mutability, value types, and so on.

00:37 The problem is that we have an untyped dictionary and we want to mutate something deep inside of its structure. For example, we want to change the name of the capital in the following dictionary:

var dict: [String:Any] = [
    "countries": [
        "japan": [
            "capital": [
                "name": "tokyo",
                "lat": "35.6895",
                "lon": "139.6917"
            ],
            "language": "japanese"
        ]
    ],
    "airports": [
        "germany": ["FRA", "MUC", "HAM", "TXL"]
    ]
]

01:04 Usually when we have a dictionary like this, we parse it into a number of structs, then change the structs, and serialize it back to a dictionary. However, today we want to directly mutate the dictionary rather than going through a conversion process. It's an interesting exercise.

Accessing the Values

01:38 Just accessing something in the dictionary is already complicated. In a language like JavaScript, you'd write something like dict["countries"]["japan"], but that doesn't work in Swift. First of all, the result of dict["countries"] is optional, so we need to use optional chaining. Second, the result is of type Any?, so we can't write the second subscript.

02:16 In order to use a subscript, we have to add an optional cast to [String:Any] so that we can use the next subscript:

(dict["countries"] as? [String:Any])?["japan"]

Before we continue, we'll simplify the dictionary just a little bit:

var dict: [String:Any] = [
    "countries": [
        "japan": [
            "capital": "tokyo"
        ]
    ]
]

02:56 To look up the capital, we have to repeat the process: cast the entire expression and use optional chaining. Finally, we can access the value:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"]

Assigning New Values

03:32 What we really want to do though is assign a new value, like this:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] = "berlin"
// doesn't compile

However, the code above doesn't work. Before we go into the reason why it breaks, we'll first make it work in a different way.

03:57 The simplest way to do this is by manually typing out all the nesting levels:

if var countries = dict["countries"] as? [String:Any],
   var japan = countries["japan"] as? [String:Any] {
    japan["capital"] = "berlin"
    countries["japan"] = japan
    dict["countries"] = countries
}

What About NSDictionary?

05:11 First, we thought mutating an untyped dictionary like this would be a lot easier using NSDictionary. We can use key-value coding, and all of the code will be simpler, so let's try that.

05:43 To access the value, we can simply use value(forKeyPath:):

import Foundation
(dict as NSDictionary).value(forKeyPath: "countries.japan.capital")

06:15 The code above is short and readable, and because we're dealing with untyped dictionaries anyway, it's just as safe as what we had before. However, it turns out we can't use KVC in the same way to change the value. The following code crashes:

(dict as NSDictionary).setValue("berlin", forKeyPath: "countries.japan.capital")
// crashes

First of all, the dictionary should be mutable:

(NSMutableDictionary(dictionary: dict)).setValue("berlin", forKeyPath: "countries.japan.capital")
// still crashes

07:14 However, this also crashes. Although the outer dictionary is now mutable, all the nested dictionaries are still immutable. That's why we crash with an error saying the instance isn't key-value coding-compliant. NSDictionary doesn't really help us here in making changes to nested untyped dictionaries when compared to Swift's Dictionary.

Casting and L-Values

07:45 In order to find a solution, we first need to understand why the single-line solution we wrote out previously doesn't work:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] = "berlin"
// doesn't compile

To explain what's going on, we'll create a simpler example. When we have a var variable of type Any, we can assign a new value:

var x: Any = 1
x = 2

08:25 As soon as we cast the variable to a different type (with the cast succeeding), we can no longer mutate it:

var x: Any = 1
(x as? Int) = 2 // doesn't compile

08:40 This is related to a concept called l-values, which are expressions that are allowed to be on the left-hand side of an assignment operator. We all know the let and var keywords, which control mutability. If we declare something with let, we can't use it as an l-value. And it turns out that there are other things that influence the "l-valueness" of a variable. For example, a cast removes the "l-valueness" of an expression, even if it's a var, like in the example above.

09:24 Next to variables declared with var, there are some other things that are l-values. For example, computed properties that have a getter and a setter are l-values. Likewise, subscripts can be used as an l-value if they have a setter. Finally, optional chaining propagates "l-valueness": if the expression was an l-value before, then after adding an optional chaining operator, it's still an l-value. We can use that knowledge to come up with a simpler solution.

Using Custom Subscripts

10:20 We use a subscript to combine the casting with providing the setter. First, we add the getter for the subscript in an extension to Dictionary. The result of the subscript is [String:Any]?:

extension Dictionary {
    subscript(jsonDict key: Key) -> [String:Any]? {
        return self[key] as? [String:Any]
    }
}

11:14 This already helps with the readability of accessing our dictionary:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?["capital"]

11:47 It's not as clean as the key-value coding version, but at least it's still simple.

11:54 We still can't mutate, and for this we have to add a setter to the subscript so that we can use the subscript as an l-value. Within the setter, we simply assign the newValue and cast it to the Value type:

extension Dictionary {
    subscript(jsonDict key: Key) -> [String:Any]? {
        get {
            return self[key] as? [String:Any]
        }
        set {
            self[key] = newValue as? Value
        }
    }
}

Now we can mutate a value within a nested dictionary, like this:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?["capital"] = "berlin"

12:47 This solution is straightforward, but we can only assign new values, not mutate existing values. Let's say we want to append to the existing string. The problem with our current expression is that its type is Any?. In order to mutate, we need a String?. Of course, once we add a type cast, it's no longer an l-value.

13:30 Luckily, we can copy-paste our existing subscript and modify it for Strings:

subscript(string key: Key) -> String? {
    get {
        return self[key] as? String
    }
    set {
        self[key] = newValue as? Value
    }
}

13:54 Now we can mutate a string value within the untyped dictionary like this:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?[string: "capital"]?.append("!")

14:17 Even though we usually wouldn't work on an untyped (e.g. JSON) dictionary such as this, it's still an interesting problem, because it forces us to think about l-values and when something is mutable. It's also interesting to have custom subscripts. In some cases, they can really help make our code more readable, and not just when dealing with all the untypedness of JSON.

Resources

  • Playground

    Written in Swift 3

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

57 Episodes · 20h06min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes