Let's say I have a rather complex dictionary, like this one:
let dict: [String: Any] = [
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lat": "35.6895",
"lon": "139.6917"
],
"language": "japanese"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL"]
]
]
I can access all fields with if let ..
blocks optionally casting to something that I can work with, when reading.
However, I am currently writing unit tests where I need to selectively break dictionaries in multiple ways.
But I don't know how to elegantly remove keys from the dictionary.
For example I want to remove the key "japan"
in one test, in the next "lat"
should be nil.
Here's my current implementation for removing "lat"
:
if var countries = dict["countries"] as? [String: Any],
var japan = countries["japan"] as? [String: Any],
var capital = japan["capital"] as? [String: Any]
{
capital.removeValue(forKey: "lat")
japan["capital"] = capital
countries["japan"] = japan
dictWithoutLat["countries"] = countries
}
Surely there must be a more elegant way?
Ideally I'd write a test helper that takes a KVC string and has a signature like:
func dictWithoutKeyPath(_ path: String) -> [String: Any]
In the "lat"
case I'd call it with dictWithoutKeyPath("countries.japan.capital.lat")
.
Interesting question. The problem seems to be that Swift's optional chaining mechanism, which is normally capable of mutating nested dictionaries, trips over the necessary type casts from
Any
to[String:Any]
. So while accessing a nested element becomes only unreadable (because of the typecasts):… mutating a nested element doesn't even work:
Possible Solution
The idea is to get rid of the untyped dictionary and convert it into a strongly typed structure where each element has the same type. I admit that this is a heavy-handed solution, but it works quite well in the end.
An enum with associated values would work well for our custom type that replaces the untyped dictionary:
The enum has one case for each expected element type. The three cases cover your example, but it could easily be expanded to cover more types.
Next, we define two subscripts, one for keyed access to a dictionary (with strings) and one for indexed access to an array (with integers). The subscripts check if
self
is a.dict
or.array
respectively and if so return the value at the given key/index. They returnnil
if the type doesn't match, e.g. if you tried to access a key of a.string
value. The subscripts also have setters. This is key to make chained mutation work:Lastly, we add some convenience initializers for initializing our type with dictionary, array or string literals. These are not strictly necessary, but make working with the type easier:
And here's the example:
Pass your dictionary to this function, It will return you a flat dictionary, without any nested dict incorporated .
//SWIFT 3.0
You could construct recursive methods (read/write) which visits your given key path by repeatedly attempting conversions of (sub-)dictionary values to
[Key: Any]
dictionaries themselves. Moreover, allow public access to these methods via a newsubscript
.Note that you might have to explicitly import
Foundation
for access to thecomponents(separatedBy:)
method ofString
(bridged).Example setup
Example usage:
Note that if a supplied key path does not exist for an assignment (using setter), this will not result in the construction of the equivalent nested dictionary, but simply result in no mutation of the dictionary.
When working with a subscript, if the subscript is get/set and the variable is mutable, then the entire expression is mutable. However, due to the type cast the expression "loses" the mutability. (It's not an l-value anymore).
The shortest way to solve this is by creating a subscript that is get/set and does the conversion for you.
Now you can write the following:
We liked this question so much that we decided to make a (public) Swift Talk episode about it: mutating untyped dictionaries
I'd to like to follow up on my previous answer with another solution. This one extends Swift's
Dictionary
type with a new subscript that takes a key path.I first introduce a new type named
KeyPath
to represent a key path. It's not strictly necessary, but it makes working with key paths much easier because it lets us wrap the logic of splitting a key path into its components.Next I create a dummy protocol named
StringProtocol
that we later need to constrain ourDictionary
extension. Swift 3.0 doesn't yet support extensions on generic types that constrain a generic parameter to a concrete type (such asextension Dictionary where Key == String
). Support for this is planned for Swift 4.0, but until then, we need this little workaround:Now we can write the new subscripts. The implementation for the getter and setter are fairly long, but they should be straightforward: we traverse the key path from beginning to end and then get/set the value at that position:
And this is how it looks in use:
I really like this solution. It's quite a lot of code, but you only have to write it once and I think it looks very nice in use.