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/
Related
until version 10.7.6 of Realm I could convert to dictionary and then to json with this code below, but the ListBase class no longer exists.
extension Object {
func toDictionary() -> NSDictionary {
let properties = self.objectSchema.properties.map { $0.name }
let dictionary = self.dictionaryWithValues(forKeys: properties)
let mutabledic = NSMutableDictionary()
mutabledic.setValuesForKeys(dictionary)
for prop in self.objectSchema.properties as [Property] {
// find lists
if let nestedObject = self[prop.name] as? Object {
mutabledic.setValue(nestedObject.toDictionary(), forKey: prop.name)
} else if let nestedListObject = self[prop.name] as? ListBase { /*Cannot find type 'ListBase' in scope*/
var objects = [AnyObject]()
for index in 0..<nestedListObject._rlmArray.count {
let object = nestedListObject._rlmArray[index] as! Object
objects.append(object.toDictionary())
}
mutabledic.setObject(objects, forKey: prop.name as NSCopying)
}
}
return mutabledic
}
}
let parameterDictionary = myRealmData.toDictionary()
guard let postData = try? JSONSerialization.data(withJSONObject: parameterDictionary, options: []) else {
return
}
List now inherits from RLMSwiftCollectionBase apparently, so you can check for that instead. Also, this is Swift. Use [String: Any] instead of NSDictionary.
extension Object {
func toDictionary() -> [String: Any] {
let properties = self.objectSchema.properties.map { $0.name }
var mutabledic = self.dictionaryWithValues(forKeys: properties)
for prop in self.objectSchema.properties as [Property] {
// find lists
if let nestedObject = self[prop.name] as? Object {
mutabledic[prop.name] = nestedObject.toDictionary()
} else if let nestedListObject = self[prop.name] as? RLMSwiftCollectionBase {
var objects = [[String: Any]]()
for index in 0..<nestedListObject._rlmCollection.count {
let object = nestedListObject._rlmCollection[index] as! Object
objects.append(object.toDictionary())
}
mutabledic[prop.name] = objects
}
}
return mutabledic
}
}
Thanks to #Eduardo Dos Santos. Just do the following steps. You will be good to go.
Change ListBase to RLMSwiftCollectionBase
Change _rlmArray to _rlmCollection
Import Realm
I have two arrays.
var searchedArray: NSMutableArray!
var libraryArray: NSMutableArray!
I'm trying to fix my searchBar functionality because it always shows nil(searchedArray = nil).
I've tried to downcast it in different ways but it doesn't work.
Here's the snippet.
let laMutableCopy = (downloadManager.libraryArray as NSArray).mutableCopy()
searchedArray = laMutableCopy.filter{
guard let dict = $0 as? Dictionary<String, Any> else {return false}
guard let title = dict["title"] as? String else {return false}
return title.range(of: searchText, options: [caseInsensitive, .anchored]) != nil
} as? NSMutableArray
Here's an example that would work:
var libraryArray = [
["title": "foo"],
["title": "bar"]
]
var filteredArray = libraryArray.filter{
// your production code doesn't have to be like this, but when attempting to debug
// its very helpful to break each cast out into its own assign to see where you
// are failing
guard let dict = $0 as? Dictionary<String, Any> else { return false }
guard let title = dict["title"] as? String else { return false }
return title.range(of: "fo", options: [.caseInsensitive, .anchored]) != nil
} as? Array<Dictionary<String, Any>>
The problem with your code is that you're casting the result of the filter (a swift Array) to an NSMutableArray. Swift arrays can be cast to NSArray, but they cannot be cast to NSMutableArray without constructing a copy like so:
(libraryArray as NSArray).mutableCopy()
I have this code:
var address = Dictionary<String, AnyObject?>();
address["address1"] = "Here";
address["address2"] = "There";
...
let defaults = NSUserDefaults.standardUserDefaults();
var data = defaults.valueForKey("active_user")! as! Dictionary<String, AnyObject?>
data["address"] = address;
defaults.setValue(data, forKey: "active_user");
defaults.synchronize();
I want to change it into like this:
var address = Dictionary<String, AnyObject?>();
address["address1"] = "Here";
address["address2"] = "There";
...
let defaults = NSUserDefaults.standardUserDefaults();
defaults["active_user"]!["address"]! = address;
defaults.synchronize();
Is this possible? How can I do that?
This is somewhat possible by extending NSUserDefaults to have a subscript. See below:
extension NSUserDefaults {
subscript(key: String) -> Any {
get {
return value(forKey: key)
}
set {
setValue(newValue, forKey: key)
}
}
}
This can then be used like so:
let defaults = NSUserDefaults.standardUserDefaults()
defaults["myKey"] = "someValue"
let myValue = defaults["myKey"]
One limitation is that you won't be able to modify nested collections as you've done in your example. You'll always have to make a manual assignment back to the user defaults object to save something.
// this won't work
defaults["active_user"]!["address"]! = address;
// do this instead
let user = defaults["active_user"]!
user["address"] = address
defaults["active_user"] = user
Edit:
I figured out a way to overload the subscript so you can modify nested collections, with slightly different but very clean syntax. Only works at one level of nesting, but I think it could be taken further. Add this to your NSUserDefaults extension:
subscript(firstKey: String, secondKey: String) -> Any? {
get {
if let dict = value(forKey: firstKey) as? [String: Any] {
return dict[secondKey]
}
return nil
}
set {
if let dict = value(forKey: firstKey) as? [String: Any] {
dict[secondKey] = newValue
setValue(dict, forKey: firstKey)
}
}
}
Then you can do this:
defaults["active_user", "address"] = "some address"
let address = defaults["active_user", "address"]
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'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
}