Forked from casperzandbergenyaacomm/DictionaryKeyPath.swift
Last active
June 15, 2025 13:20
-
-
Save Amzd/e908d4bf3cbad2c9d766586baa619566 to your computer and use it in GitHub Desktop.
Reading and writing to (possible) nested dictionaries for a path of keys, using a recursive approach
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension Dictionary { | |
public subscript(path string: Key, separator separator: String) -> Value? where Key == String { | |
get { self[path: string.split(separator: separator).map(String.init)] } | |
set { self[path: string.split(separator: separator).map(String.init)] = newValue } | |
} | |
public subscript(path path: Key...) -> Value? { | |
get { self[path: path] } | |
set { self[path: path] = newValue } | |
} | |
public subscript(path path: [Key]) -> Value? { | |
get { getValue(forPath: path) } | |
set { | |
setValue(newValue, forPath: path) | |
if newValue == nil { | |
cleanUp(forPath: path) | |
} | |
} | |
} | |
/// recursively (attempt to) access queried subdictionaries | |
private func getValue(forPath path: [Key]) -> Value? { | |
if path.count == 1, let key = path.first { | |
return self[key] | |
} else if let key = path.first { | |
let next = self[key] as? Self | |
return next?.getValue(forPath: Array(path.dropFirst())) | |
} else { | |
return nil | |
} | |
} | |
/// recursively access, create or overwrite the queried subdictionaries to finally set the "inner value" | |
private mutating func setValue(_ value: Value?, forPath path: [Key]) { | |
if path.count == 1, let key = path.first { | |
self[key] = value | |
} else if let key = path.first { | |
var subDict = self[key] as? Self ?? Self() | |
subDict.setValue(value, forPath: Array(path.dropFirst())) | |
self[key] = subDict as? Value | |
} | |
} | |
/// (attempt to) remove left over empty subdictionaries | |
private mutating func cleanUp(forPath path: [Key]) { | |
var path = path | |
while !path.isEmpty { | |
if let value = getValue(forPath: path) { | |
// Stop cleanUp unless an empty dict is found | |
guard let dict = value as? [Key: Any], dict.isEmpty else { break } | |
setValue(nil, forPath: path) | |
} | |
path = path.dropLast() | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var dict: [String: Any] = [ | |
"some": [ | |
"nested": [ | |
"data": "Often found in json" | |
] | |
] | |
] | |
let a = dict[keyPath: "some/nested/data", separator: "/"] // "Often found in json" | |
let b = dict[keyPath: "some", "nested", "data"] // "Often found in json" | |
let c = dict[keyPath: ["some", "nested", "data"]] // "Often found in json" | |
dict[keyPath: "some/nested/data", separator: "/"] = "Replacing data" | |
dict[keyPath: "another/nested/path", separator: "/"] = "Creating new subdirectories" | |
print(dict as AnyObject) | |
/* | |
{ | |
another = { | |
nested = { | |
path = "Creating new subdirectories"; | |
}; | |
}; | |
some = { | |
nested = { | |
data = "Replacing data"; | |
}; | |
}; | |
} | |
*/ | |
// Clean up removes all empty subdirectories | |
dict[keyPath: "some/nested/data", separator: "/"] = nil | |
print(dict as AnyObject) | |
/* | |
{ | |
another = { | |
nested = { | |
path = "Creating new subdirectories"; | |
}; | |
}; | |
} | |
*/ | |
// Warning: I opted to allow overwriting of data | |
dict[keyPath: "another/nested/path/new/path", separator: "/"] = "Overwrote path node" | |
print(dict as AnyObject) | |
/* | |
{ | |
another = { | |
nested = { | |
path = { | |
new = { | |
path = "Overwrote path node"; | |
}; | |
}; | |
}; | |
}; | |
} | |
*/ | |
// This also means if we set a deeper node to nil | |
// cleanup will delete higher empty levels since | |
// those are overwritten | |
dict[keyPath: "another/nested/path/new/path/even/deeper/path", separator: "/"] = nil | |
print(dict as AnyObject) | |
/* | |
{ | |
} | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated from casperzandbergenyaacomm/DictionaryKeyPath.swift
The separator is now explicit so you can use this when your keys contain "/"