0:06 Today, let's have a look at mutating untyped dictionaries. This is a
question we found on
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:
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.
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:
Before we continue, we'll simplify the dictionary just a little bit:
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:
(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
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:
8:25 As soon as we cast the variable to a different type (with the cast
succeeding), we cannot mutate it anymore:
varx:Any=1(xas?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]?:
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:
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
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