Skip to content

Instantly share code, notes, and snippets.

@Amzd
Forked from casperzandbergenyaacomm/DictionaryKeyPath.swift
Last active November 5, 2023 04:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Amzd/e908d4bf3cbad2c9d766586baa619566 to your computer and use it in GitHub Desktop.
Save Amzd/e908d4bf3cbad2c9d766586baa619566 to your computer and use it in GitHub Desktop.
Reading and writing to (possible) nested dictionaries for a given key path, using a recursive approach
// Inspired by: https://gist.github.com/dfrib/d7419038f7e680d3f268750d63f0dfae
import Foundation
public extension Dictionary {
subscript(keyPath string: Key, separator: String) -> Value? where Key == String {
get { return self[keyPath: string.components(separatedBy: separator)] }
set { self[keyPath: string.components(separatedBy: separator)] = newValue }
}
subscript(keyPath keyPath: Key...) -> Value? {
get { return self[keyPath: keyPath] }
set { self[keyPath: keyPath] = newValue }
}
subscript(keyPath keyPath: [Key]) -> Value? {
get {
guard !keyPath.isEmpty else { return nil }
return getValue(forKeyPath: keyPath)
}
set {
guard !keyPath.isEmpty else { return }
setValue(newValue, forKeyPath: keyPath)
if newValue == nil {
cleanUp(forKeyPath: keyPath)
}
}
}
// recursively (attempt to) access queried subdictionaries
// (keyPath will never be empty here; the explicit unwrapping is safe)
private func getValue(forKeyPath keyPath: [Key]) -> Value? {
if keyPath.count == 1 {
return self[keyPath.first!]
} else {
let next = self[keyPath.first!] as? Self
return next?.getValue(forKeyPath: Array(keyPath.dropFirst()))
}
}
// recursively access, create or overwrite the
// queried subdictionaries to finally set the "inner value"
// (keyPath will never be empty here; the explicit unwrapping is safe)
private mutating func setValue(_ value: Value?, forKeyPath keyPath: [Key]) {
if keyPath.count == 1 {
self[keyPath.first!] = value
} else {
var subDict = self[keyPath.first!] as? Self ?? Self()
subDict.setValue(value, forKeyPath: Array(keyPath.dropFirst()))
self[keyPath.first!] = subDict as? Value
}
}
// recursively (attempt to) remove left over empty subdictionaries
private mutating func cleanUp(forKeyPath keyPath: [Key]) {
// Never set root to nil
guard !keyPath.isEmpty else { return }
if let value = getValue(forKeyPath: keyPath) {
guard let dict = value as? [Key: Value], dict.isEmpty else {
// This endpoint does not continue cleanUp because
// a non nil value that isn't an empty dict is found
return
}
setValue(nil, forKeyPath: keyPath)
}
cleanUp(forKeyPath: Array(keyPath.dropLast()))
}
}
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)
/*
{
}
*/
@Amzd
Copy link
Author

Amzd commented Jun 14, 2021

Updated from casperzandbergenyaacomm/DictionaryKeyPath.swift

The separator is now explicit so you can use this when your keys contain "/"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment