Forked from casperzandbergenyaacomm/DictionaryKeyPath.swift
Last active
November 5, 2023 04:46
-
-
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
This file contains 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
// 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())) | |
} | |
} |
This file contains 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 "/"