0:06 Today, let's have a look at mutating untyped dictionaries. This is a
question we found on
StackOverflow,
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: how Swift works with mutability, value types, and so on.
0: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"]
]
]
1:04 Usually, when we have a dictionary like this, we'd 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
1:38 Just accessing something in the dictionary is already complicated.
In a language like Javascript, you would 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.
2: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"
]
]
]
2: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
3:32 What we really want to do though is to 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.
3:57 The simplest way to make it work is typing out all the nesting
levels manually:
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?
5: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 would be simpler, so let's try that.
5:43 To access the value, we can simply use value(forKeyPath:)
:
import Foundation
(dict as NSDictionary).value(forKeyPath: "countries.japan.capital")
6: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
7: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 compared to Swift's Dictionary
in
making changes to nested untyped dictionaries.
Casting and l-values
7: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 more simple example. When we have a
var
variable of type Any
, we can assign a new value:
var x: Any = 1
x = 2
8:25 As soon as we cast the variable to a different type (with the cast
succeeding), we cannot mutate it anymore:
var x: Any = 1
(x as? Int) = 2 // doesn't compile
8:40 This is related to a concept called l-values. l-values 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
you declare something with let
, we can't use it as an l-value. 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.
9:24 Next to variables decalred 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
not an l-value anymore.
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 woudn't work on an untyped (e.g. JSON)
dictionary like this, it's still an interesting problem, because we were forced
to think about l-values, and when something is mutable. It's also interesting to
have these custom subscripts. In some cases they can really help to make our
code more readable. These are some good tools to deal with all the untypedness
of JSON.