I've been working on a recursive function to extract String values out of JSON data represented as an NSDictionary. The function allows you to do this:
if let value = extractFromNestedDictionary(["fee" : ["fi" : ["fo" : "fum"]]], withKeys: ["fee", "fi", "fo"]) {
println("\(value) is the value after traversing fee-fi-fo");
}
And the function implementation looks like this:
// Recursively retrieves the nested dictionaries for each key in `keys`,
// until the value for the last key is retrieved, which is returned as a String?
func extractFromNestedDictionary(dictionary: NSDictionary, withKeys keys: [String]) -> String? {
if keys.isEmpty { return nil }
let head = keys[0]
if let result: AnyObject = dictionary[head] {
if keys.count == 1 {
return result as? String
} else {
let tail: [String] = Array(keys[1..<keys.count])
if let result = result as? NSDictionary {
return extractFromNestedDictionary(result, withKeys: tail)
} else {
return nil
}
}
} else {
return nil
}
}
Are there some syntactical features related to optional binding in Swift 1.2/2.x that can:
make this function more succinct
use less if nesting
Instead of recursion, you can use reduce on the keys array
to traverse through the dictionary:
func extractFromNestedDictionary(dictionary: NSDictionary, withKeys keys: [String]) -> String? {
return reduce(keys, dictionary as AnyObject?) {
($0 as? NSDictionary)?[$1]
} as? String
}
Inside the closure, $0 is the (optional) object on the current level and $1
the current key. The closure returns the object on the next level
if $0 is a dictionary and has a value for the current key,
and nil otherwise. The return value from reduce() is then
the object on the last level or nil.
I originally didn't want to just make it without you trying first, but I did it anyways because I love Swift and had fun doing it:
func extractFromNestedDictionary(dictionary: [NSObject : AnyObject], var withKeys keys: [String]) -> String? {
if let head = keys.first, result = dictionary[head] {
if keys.count == 1 {
return result as? String
} else if let result = result as? [NSObject : AnyObject] {
keys.removeAtIndex(0)
return extractFromNestedDictionary(result, withKeys: keys)
}
}
return nil
}
extractFromNestedDictionary(["A" : ["B" : ["C" : "D"]]], withKeys: ["A", "B", "C"])
A few notes:
Try to avoid NSDictionary and use [NSObject : AnyObject] instead, which can be bridged to NSDictionary anyways and is much more Swifty
When asking a question, try to make a better example than you did there, from your example I wasn't able to know what exactly you want to do.
I know this isn't strictly answering the question. But you could just use valueForKeypath:
let fum = dict.valueForKeyPath("fee.fi.fo")
Here's the best I came up with...
func extractFromNestedDictionary(dictionary: [NSObject : AnyObject], withKeys keys: [String]) -> String? {
if let head = keys.first,
result = dictionary[head] as? String
where keys.count == 1 {
return result
} else if let head = keys.first,
result = dictionary[head] as? [NSObject : AnyObject] {
return extractFromNestedDictionary(result, withKeys: Array(keys[1..<keys.count]))
}
return nil
}
Related
I have questions and answers in Firebase Realtime Database, which I need to be downloaded and put in to arrays. I have created a function which takes a parameter and should return two arrays
let data = fetchQuestions(category: "Animals")
qAni = data.ques
aAni = data.ans
fetchQuestions method:
func fetchQuestions(category: String) -> (ques: [String], ans: [String]) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
return (q, a) ** Unexpected non-void return value in void function
}
}
})
}
I have tried to use dispatch group, but then I receive a different error.
You are currently returning from the completion block of observeSingleEvent and its return type is void, What you need is you need to create completionHandler with your fetchQuestions. Now with Swift instead of using NSArray use Swift native array. Also instead of maintaining two array maintain one array with custom objects of struct or class.
So first create one struct like this.
struct Category {
let question: String
let answer: String
}
Now make completionHandler with your fetchQuestions like this way.
func fetchQuestions(category: String, completion: #escaping([Category]) -> Void) {
var array = [Category]
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! [[String:String]]
for i in QA {
let que = i["question"]!
let ans = i["answer"]!
array.append(Category(question: que, answer: ans))
}
}
}
completion(array)
})
}
Now call fetchQuestions this way.
self.fetchQuestions(category: "Animals") { categories in
//access categories here
print(categories)
}
Note: Instead of comparing value in for loop you can also use queryEqual(toValue:) with your request to get specific result check this SO thread for more details on it.
Its because you are returning a value from a closure instead you should use different closure for that. Try it:
func fetchQuestions(category: StringcompletionBlock:#escaping (_ ques:[String], _ ans :[String]) -> ()) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
}
return completionBlock(q,a)
}
})
}
#escaping is required : when the closure is passed as an argument to
the function, but is called after the function returns.
I am new to Swift. I tried with this Swift link Detect a Null value in NSDictionaryNSDictionary, but I failed to do so.
Data:
"end_time" = "<null>"
Here is my code:
if endTime["end_time"] is NSNull {
print("your session still available ")
}
else{
print("your session end \(endTime["end_time"])")
}
Every time it is going to else statement. May be I need to convert string to null or alternative solution. Could you help me please?
Thank you.
Here's how you check null in swift:
let time = endTime["end_time"]
if time != "<null>" {
print("time is not <null>")
}
else
{
print("time is <null>")
}
You can create a NilCheck controller to check nil or null for various datatypes. For example i have created a function to remove null [if any] from the dictionary and store the array of dictionary in Userdefaults. Please be free to ask your queries :)
func removeNilAndSaveToLocalStore(array : [[String:Any]]) {
var arrayToSave = [[String:Any]]()
for place in array {
var dict = [String:Any]()
dict["AreaId"] = NilCheck.sharedInstance.checkIntForNil(nbr: place["AreaId"]! as? Int)
dict["AreaNameAr"] = NilCheck.sharedInstance.checkStringForNil(str: place["AreaNameAr"]! as? String)
dict["AreaName"] = NilCheck.sharedInstance.checkStringForNil(str: place["AreaName"]! as? String)
dict["GovernorateId"] = NilCheck.sharedInstance.checkIntForNil(nbr: place["GovernorateId"]! as? Int)
arrayToSave.append(dict)
}
LocalStore.setAreaList(token: arrayToSave)
}
class NilCheck {
static let sharedInstance : NilCheck = {
let instance = NilCheck()
return instance
}()
func checkStringForNil(str : String?) -> String {
guard let str = str else {
return String() // return default string
}
return str
}
func checkIntForNil(nbr : Int?) -> Int {
guard let num = nbr else {
return 0 // return default Int
}
return num
} }
I'm experienced in Obj-C, but fairly new to Swift. I have a simple function that takes a Set and a Dictionary as parameters:
func buildSource(dataToParse:Set<String>, lookupData:Dictionary<String,AnyObject>) {
for item in dataToParse {
for dict in lookupData {
let nameID = dict["name"] // compile error
}
}
}
The passed in parameter lookupData is a dictionary containing nested dictionaries. I know that each of these dictionaries contains a key called name but when I try to access that key using the following syntax:
let nameID = dict["name"]
I get the following comile error:
Type '(String, AnyObject)' has no subscript members
If I know that a key exists, how do I access it? Thanks!
import Foundation
func buildSource(dataToParse:Set<String>, lookupData:Dictionary<String,AnyObject>)->[AnyObject] {
var ret: Array<AnyObject> = []
for item in dataToParse {
for (key, value) in lookupData {
if key == item {
ret.append(value)
}
}
}
return ret
}
let set = Set(["alfa","beta","gama"])
var data: Dictionary<String,AnyObject> = [:]
data["alfa"] = NSNumber(integer: 1)
data["beta"] = NSDate()
data["theta"] = NSString(string: "some string")
let find = buildSource(set, lookupData: data)
dump(find)
/* prints
▿ 2 elements
- [0]: 28 Nov 2015 18:02
▿ [1]: 1 #0
▿ NSNumber: 1
▿ NSValue: 1
- NSObject: 1
*/
in your code
for dict in lookupData {
let nameID = dict["name"] // compile error
}
dict is not a dictionary, but (key, value) tuple!
Update:
get value from lookupData with "name" as the key
downcast to a dictionary with as? [String:AnyObject]
iterate over dataToParse
use the String from dataToParse to access the nested dictionary.
func buildSource(dataToParse:Set<String>, lookupData:[String:AnyObject]) {
guard let dict = lookupData["name"] as? [String:AnyObject] else {
return
}
for item in dataToParse {
let value = dict[item]
}
}
More possible solutions:
If lookupData is an array of dictionaries:
func buildSource(dataToParse:Set<String>, lookupData:[[String:AnyObject]]) {
for item in dataToParse { // String
for dict in lookupData { // dict
let nameID = dict["name"] // value
}
}
}
If lookupData is a nested dictionary :
func buildSource(dataToParse:Set<String>, lookupData:[String:[String:AnyObject]]) {
for item in dataToParse {
guard let dict = lookupData[item] else {
continue
}
guard let nameID = dict["name"] else {
continue
}
}
}
If lookupData is definitely [String:AnyObject] and it's value might be another [String:AnyObject], but it might also not be.
func buildSource(dataToParse:Set<String>, lookupData:[String:AnyObject]) {
for item in dataToParse {
guard let dict = lookupData[item] as? [String:AnyObject] else {
continue
}
guard let nameID = dict["name"] else {
continue
}
}
}
I am parsing a dictionary with stuff to generate a Rocket object. In the parsing method i use guard to do my checks. Is it possible to use guard to both check for existing value in a dictionary, and unwrap it at the same time?
let rocketDictionary : [String : String?] = [ "name" : nil, "numberOfThrusters" : nil ]
func generateRocketSchematics(rocketDictionary : [String : String?]) {
guard let rocketName = rocketDictionary["name"] as? String else {
print("no rocket name")
return
}
print(rocketName)
}
This code won't compile because of the as? String. If i remove that, the rocketName will be a String? Is it possible to write the guard statement so that it will return a String
You can use pattern matching with guard/case:
func generateRocketSchematics(rocketDictionary : [String : String?]) {
guard case let rocketName?? = rocketDictionary["name"] else {
print("no rocket name")
return
}
print(rocketName) // rocketName is a String
}
rocketName?? is a synonym for .Some(.Some(rocketName)), so
case let rocketName?? = rocketDictionary["name"]
matches the case where rocketDictionary["name"] (which has the
type String??) is not nil, and the unwrapped value (which has
the type String?) is also not nil. The doubly-unwrapped value
(of type String) is then assigned to rockedName.
I want to safely search through values in a swift Dictionary using if lets and making sure it is type safe as I get deeper and deeper into the dictionary. The dictionary contains dictionaries that contains NSArray that contains more dictionary.
At first attempt my code looks like this:
if let kkbox = ticket["KKBOX"] as? Dictionary<String, AnyObject> {
if let kkboxDlUrlDict = kkbox["kkbox_dl_url_list"] as? Dictionary<String, AnyObject> {
if let kkboxDlUrlArray = kkboxDlUrlDict["kkbox_dl_url"] as? NSArray {
for dict in kkboxDlUrlArray {
if let name = dict["name"] as? String {
if name == mediaType.rawValue {
urlStr = dict["url"] as String
}
}
}
} else { return nil }
} else { return nil }
} else { return nil }
How do I shorten it to perhaps one or 2 line?
I realised I can chain it if it is 2 layers. This works:
if let kkboxDlUrlArray = ticket["KKBOX"]?["kkbox_dl_url_list"] as? NSArray {
}
But any chain longer than that, will not compile.
Is there a way to chain through a dictionary more than once?
Thank you
You can chain, but with proper downcast at each step:
if let kkboxDlUrlArray = ((((ticket["KKBOX"] as? Dictionary<String, AnyObject>)?["kkbox_dl_url_list"]) as? Dictionary<String, AnyObject>)?["kkbox_dl_url"]) as? NSArray {
for dict in kkboxDlUrlArray {
println(dict)
}
}
That doesn't look good though - it's one line, but not readable.
Personally, without using any fancy functional way to do it, I would make the chain more explicit with just one optional binding:
let kkbox = ticket["KKBOX"] as? Dictionary<String, AnyObject>
let kkboxDlUrlDict = kkbox?["kkbox_dl_url_list"] as? Dictionary<String, AnyObject>
if let kkboxDlUrlArray = kkboxDlUrlDict?["kkbox_dl_url"] as? NSArray {
for dict in kkboxDlUrlArray {
println(dict)
}
}
In my opinion much easier to read, and with no unneeded indentation
Alternatively, you could use this extension, which mimics the original valueForKeyPath method that used to exist in NSDictionary but got axed for whatever reason:
Swift 4.1
extension Dictionary where Key: ExpressibleByStringLiteral {
func valueFor(keyPath: String) -> Any? {
var keys = keyPath.components(separatedBy: ".")
var val : Any = self
while keys.count > 0 {
if let key = keys[0] as? Key {
keys.remove(at: 0)
if let dic = val as? Dictionary<Key, Value> {
if let leaf = dic[key] {
val = leaf
} else {
return nil
}
} else {
return nil
}
} else {
return nil
}
}
return val
}
Your code would then read:
if let array = ticket.valueFor("KKBOX.kkbox_dl_url_list.kkbox_dl_url") as? [] {
// do something with array
}
Seems like swift 1.2 has added this feature.
"More powerful optional unwrapping with if let — The if let construct can now unwrap multiple optionals at once, as well as include intervening boolean conditions. This lets you express conditional control flow without unnecessary nesting."
https://developer.apple.com/swift/blog/