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"
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")
First of all, the dictionary should be mutable:
(NSMutableDictionary(dictionary: dict)).setValue("berlin", forKeyPath: "countries.japan.capital")
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"
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
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 String
s:
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.