Interest Ask. The problem is that the Swift fast binding mechanism, which is usually capable of mutating nested dictionaries, disables the necessary types from Any to [String:Any] . Therefore, when access to a nested element only becomes unreadable (due to types):
// Eg Accessing countries.japan.capital ((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"]
... the mutation of the nested element does not even work:
// Want to mutate countries.japan.capital.name. // The typecasts destroy the mutating optional chaining. ((((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] as? [String:Any])?["name"] as? String) = "Edo" // Error: Cannot assign to immutable expression
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 this is a tough decision, but in the end it works great.
An enumeration with related values ββwill work well for our custom type, which replaces an untyped dictionary:
enum KeyValueStore { case dict([String: KeyValueStore]) case array([KeyValueStore]) case string(String)
An enumeration has one case for each type of expected element. Three cases cover your example, but you can easily expand it to cover more types.
Next, we define two indexes: one for the dictionary access key (with strings) and one for indexed access to the array (with integers). The subscripts check to see if self .dict or .array respectively, and if so, return the value to the given key / index. They return nil if the type does not match, for example. if you tried to access the .string value. Substrings also have setters. This is the key to working with a mutation in a chain:
extension KeyValueStore { subscript(_ key: String) -> KeyValueStore? { // If self is a .dict, return the value at key, otherwise return nil. get { switch self { case .dict(let d): return d[key] default: return nil } } // If self is a .dict, mutate the value at key, otherwise ignore. set { switch self { case .dict(var d): d[key] = newValue self = .dict(d) default: break } } } subscript(_ index: Int) -> KeyValueStore? { // If self is an array, return the element at index, otherwise return nil. get { switch self { case .array(let a): return a[index] default: return nil } } // If self is an array, mutate the element at index, otherwise return nil. set { switch self { case .array(var a): if let v = newValue { a[index] = v } else { a.remove(at: index) } self = .array(a) default: break } } } }
Finally, we add some convenience initializers to initialize our type with a dictionary, array, or string literals. They are not strictly necessary, but make working with a type easier:
extension KeyValueStore: ExpressibleByDictionaryLiteral { init(dictionaryLiteral elements: (String, KeyValueStore)...) { var dict: [String: KeyValueStore] = [:] for (key, value) in elements { dict[key] = value } self = .dict(dict) } } extension KeyValueStore: ExpressibleByArrayLiteral { init(arrayLiteral elements: KeyValueStore...) { self = .array(elements) } } extension KeyValueStore: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .string(value) } init(extendedGraphemeClusterLiteral value: String) { self = .string(value) } init(unicodeScalarLiteral value: String) { self = .string(value) } }
And here is an example:
var keyValueStore: KeyValueStore = [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lat": "35.6895", "lon": "139.6917" ], "language": "japanese" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL"] ] ] // Now optional chaining works: keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("tokyo")) keyValueStore["countries"]?["japan"]?["capital"]?["name"] = "Edo" keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("Edo")) keyValueStore["airports"]?["germany"]?[1] // .some(.string("MUC")) keyValueStore["airports"]?["germany"]?[1] = "BER" keyValueStore["airports"]?["germany"]?[1] // .some(.string("BER")) // Remove value from array by assigning nil. I'm not sure if this makes sense. keyValueStore["airports"]?["germany"]?[1] = nil keyValueStore["airports"]?["germany"] // .some(array([.string("FRA"), .string("HAM"), .string("TXL")]))