Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Last active May 11, 2021 18:37
Show Gist options
  • Save brennanMKE/e63f838241b9437f5c3cde7b9d6fa357 to your computer and use it in GitHub Desktop.
Save brennanMKE/e63f838241b9437f5c3cde7b9d6fa357 to your computer and use it in GitHub Desktop.
Path Reader for Dictionaries and Arrays as well as Info.plist

Path Reader for Dictionaries and Arrays

It can take a lot of code to access the values stored in an app's Info.plist when it is nested. It would be better to use a single string to reach each value and then reduce it down to a short string. It has extensions for Dictionary, Array and Bundle.

See the examples of how this code is used below the implementation.

By default the separator uses "." to split the path into each of the keys. A different separator character can be provided or even custom code to separate the path into keys.

import Foundation
typealias AnyDictionary = [String: Any]
typealias AnyArray = [Any]
enum PathSeparator {
case character(Character)
case custom((String) -> [String])
func separate(path: String) -> [String] {
let keys: [String]
switch self {
case .character(let character):
keys = path.split(separator: character).map { "\($0)" }
case .custom(let closure):
keys = closure(path)
}
return keys
}
}
struct PathReader {
let payload: Any
func value<T>(at path: String, pathSeparator: PathSeparator = .character(".")) -> T? {
var keys = pathSeparator.separate(path: path)
guard let valueKey = keys.popLast() else {
return nil
}
var current: Any? = payload
while current != nil && !keys.isEmpty {
guard let payload = current else {
break
}
if let key = keys.first {
keys.remove(at: 0)
current = getValue(key: key, payload: payload)
}
}
if let payload = current,
let value = getValue(key: valueKey, payload: payload) as? T {
return value
} else {
return nil
}
}
private func getValue(key: String, payload: Any) -> Any? {
let result: Any?
if let array = payload as? AnyArray,
let index = Int(key),
index < array.count {
result = array[index]
} else if let dictionary = payload as? AnyDictionary {
result = dictionary[key]
} else {
result = nil
}
return result
}
}
extension Bundle {
func value<T>(at path: String, pathSeparator: PathSeparator = .character(".")) -> T? {
guard let infoDictionary = Bundle.main.infoDictionary else {
return nil
}
let reader = PathReader(payload: infoDictionary)
if let value: T = reader.value(at: path, pathSeparator: pathSeparator) {
return value
} else {
return nil
}
}
}
extension Dictionary {
func value<T>(at path: String, pathSeparator: PathSeparator = .character(".")) -> T? {
let reader = PathReader(payload: self)
if let value: T = reader.value(at: path, pathSeparator: pathSeparator) {
return value
} else {
return nil
}
}
}
extension Array {
func value<T>(at path: String, pathSeparator: PathSeparator = .character(".")) -> T? {
let reader = PathReader(payload: self)
if let value: T = reader.value(at: path, pathSeparator: pathSeparator) {
return value
} else {
return nil
}
}
}
//: ---
let oneDictionary: AnyDictionary = [
"One": 1
]
if let number: Int = oneDictionary.value(at: "One") {
print("Value is \(number)")
} else {
print("Value not found")
}
print("---")
let deeperDictionary: AnyDictionary = [
"A": [
"B": [
"C": "ABC"
]
]
]
if let string: String = deeperDictionary.value(at: "A.B.C") {
print("Value is \(string)")
} else {
print("Value not found")
}
print("Custom Separator:")
let customSeparator: (String) -> [String] = {
$0.split(separator: "_").map { "\($0)" }
}
if let string: String = deeperDictionary.value(at: "A_B_C", pathSeparator: .custom(customSeparator)) {
print("Value is \(string)")
} else {
print("Value not found")
}
print("---")
// include an array
let mixedDictionary: AnyDictionary = [
"A": [
"B": [
["C": "ABC"],
["D": "DEF"]
]
]
]
if let array: AnyArray = mixedDictionary.value(at: "A.B") {
print("Value is \(array)")
} else {
print("Value not found")
}
if let cString: String = mixedDictionary.value(at: "A.B.0.C") {
print("Value is \(cString)")
} else {
print("Value not found")
}
if let dString: String = mixedDictionary.value(at: "A.B.1.D") {
print("Value is \(dString)")
} else {
print("Value not found")
}
print("---")
let nestedArray = [
[1, 2, 3],
[4, 5, 6]
]
if let nestedNumber: Int = nestedArray.value(at: "0.2") {
print("Value is \(nestedNumber)")
}
else {
print("Value not found")
}
if let firstArray: [Int] = nestedArray.value(at: "0") {
print("Value is \(firstArray)")
}
else {
print("Value not found")
}
if let secondArray: [Int] = nestedArray.value(at: "1") {
print("Value is \(secondArray)")
}
else {
print("Value not found")
}
enum InfoDictionary {
enum MyApp {
private static var infoDictionary: [String: Any]? {
// Use Bundle.main.infoDictionary in real app
[
"MyApp": [
"Scheme": "https://",
"Host": "api.myhost.com",
"Auth": "AES256",
"Locales": [
"en-US",
"es-MX"
]
]
]
}
static var Scheme: String? {
infoDictionary?.value(at: "MyApp.Scheme")
}
static var Host: String? {
infoDictionary?.value(at: "MyApp.Host")
}
static var Auth: String? {
infoDictionary?.value(at: "MyApp.Auth")
}
static var Locales: [String]? {
infoDictionary?.value(at: "MyApp.Locales")
}
}
}
InfoDictionary.MyApp.Scheme
InfoDictionary.MyApp.Host
InfoDictionary.MyApp.Auth
InfoDictionary.MyApp.Locales
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment