Using flatMap with dictionaries produces tuples? - ios

I have this pretty basic and straight forward mapping of an object into dictionary. I am using and parsing dictionary on the top level. One of its fields is an array of other dictionaries. To set them I use flatMap which seems an appropriate method but since the object inside is not nullable this method suddenly returns tuples (at least it seems that way) instead of dictionaries.
I created a minimum example that can be pasted into a new project pretty much anywhere any of your execution occurs which should give better detail then any description:
func testFlatMap() -> Data? {
class MyObject {
let a: String
let b: Int
init(a: String, b: Int) { self.a = a; self.b = b }
func dictionary() -> [String: Any] {
var dictionary: [String: Any] = [String: Any]()
dictionary["a"] = a
dictionary["b"] = b
return dictionary
}
}
let objects: [MyObject] = [
MyObject(a: "first", b: 1),
MyObject(a: "second", b: 2),
MyObject(a: "third", b: 3)
]
var dictionary: [String: Any] = [String: Any]()
dictionary["objects"] = objects.flatMap { $0.dictionary() }
dictionary["objects2"] = objects.map { $0.dictionary() }
print("Type of first array: " + String(describing: type(of: dictionary["objects"]!)))
print("Type of first element: " + String(describing: type(of: (dictionary["objects"] as! [Any]).first!)))
print("Type of second array: " + String(describing: type(of: dictionary["objects2"]!)))
print("Type of second element: " + String(describing: type(of: (dictionary["objects2"] as! [Any]).first!)))
return try? JSONSerialization.data(withJSONObject: dictionary, options: [])
}
_ = testFlatMap()
So this code crashes saying
'NSInvalidArgumentException', reason: 'Invalid type in JSON write
(_SwiftValue)'
(Using do-catch makes no difference which is the first WTH but let's leave that for now)
So let's look at what the log said:
Type of first array: Array<(key: String, value: Any)>
Type of first element: (key: String, value: Any)
Type of second array: Array<Dictionary<String, Any>>
Type of second element: Dictionary<String, Any>
The second one is what we expect but the first just has tuples in there. Is this natural, intentional?
Before you go all out on "why use flatMap for non-optional values" let me explain that func dictionary() -> [String: Any] used to be func dictionary() -> [String: Any]? because it skipped items that were missing some data.
So only adding that little ? at the end of that method will change the output to:
Type of first array: Array<Dictionary<String, Any>>
Type of first element: Dictionary<String, Any>
Type of second array: Array<Optional<Dictionary<String, Any>>>
Type of second element: Optional<Dictionary<String, Any>>
which means the first solution is the correct one. And on change there are no warnings, no nothing. The app will suddenly just start crashing.
The question at the end is obviously "How to avoid this?" without too much work. Using dictionary["objects"] = objects.flatMap { $0.dictionary() } as [[String: Any]] seems to do the trick to preserve one-liner. But using this seems very silly.

I see that your question boils down to map vs. flatMap. map is the more consistent one and it works as expected here so I won't delve into it.
Now on to flatMap: your problem is one of the reasons that SE-0187 was proposed. flatMap has 3 overloads:
Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element] where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
When your dictionary() function returns a non-optional, it uses the first overload. Since a dictionary is a sequence of key-value tuples, an array of tuple is what you get.
When your dictionary() function returns an optional, it uses the third overload, which essentially filters out the nils. The situation can be very confusing so SE-0187 was proposed (and accepted) to rename this overload to compactMap.
Starting from Swift 4.1 (Xcode 9.3, currently in beta), you can use compactMap:
// Assuming dictionary() returns [String: Any]?
dictionary["objects"] = objects.compactMap { $0.dictionary() }
For Swift < 4.1, the solution you provided is the only one that works, because it gives the compiler a hint to go with the third overload:
dictionary["objects"] = objects.flatMap { $0.dictionary() } as [[String: Any]]

Related

i'm trying to change the value in one dictionary by comparing the key from another dictionary

var clariQuestion: [String: String] = ["0": "What are my last 10 transactions",
"1": "What are my spending transactions",
"2": "What are my largest trasactions",
"3": "What are my pending tracnstions"]
var explanationActionParameters: [String: Any] = ["clariQuestion": 2]
So i need to match the value of explanationActionParameters with the key of clariQuestion, and then when the keys match replace the number 2 in explanationActionParameters with "what are my largest transactions". What's causing me difficulty is that the value in explanationActionParameter is of type 2 and the key in clariQuestion is of type String. Not to sure how to go about this.
You can either convert your int to string, using String(2) or convert your string to int, like Int("2")
My answer to you would be to transform your clariQuestion dictionary into an array, because the keys are integers, and an array can be seen as a dictionary where the keys are integers. So, you could do
var clariQuestion: [String] = ["What are my last 10 transactions",
"What are my spending transactions",
"What are my largest trasactions",
"What are my pending tracnstions"]
var explanationActionParameters: [String: Any] = ["clariQuestion": 2]
let questionIndex = explanationActionParameters["clariQuestion"]
let question = clariQuestion[questionIndex]
If you insist on maintaining this dictionary structure, you can flatmap your dictionaries and transform all keys from string to int, like this
clariQuestion.flatMap { (key, value) in [Int(key) : value] }
or transform all your values to a string, like this
explanationActionParameters.flatMap { (key, value) in [key : String(value)] }
Either way, now you should be able to access elements across those dictionaries. I'm going to create a function demonstrating how
func replace(_ clariQuestion: [String : String], _ explanationActionParameters: [String: Any]) -> [String: String] {
// The dictionary that will be returned containing the mapping from clariQuestion to explanationActionParameters
var ret = [String : String]()
let mappedClariQuestion = clariQuestion.flatMap { (key, value) in [Int(key) : value] }
for (key, value) in clariQuestion {
ret[key] = mappedClariQuestion[value]
}
return ret
}
Then, just explanationActionParameters = replace(clariQuestion, explanationActionParameters)

How can I cast an array of [AnyObject] to an array of [Hashable] or [AnyHashable] in Swift?

I have an array of AnyObjects ([AnyObject]). I know most of these are Hashable. How would I "cast" this AnyObject to a Hashable/AnyHashable?
AnyHashable only takes a Hashable object as a parameter.
To cast one object to another you use as. But the objects that can not be automatically converted (which is your case) you need to use either as? or as!. So the most simple way:
let hashableArray: [AnyHashable] = array as! [AnyHashable] // Will crash if even one object is not convertible to AnyHashable
let hashableArray: [AnyHashable]? = array as? [AnyHashable] // Will return nil if even one object is not convertible to AnyHashable
So one may crash while the other one will produce an optional array.
Since you wrote "I know most of these are Hashable" I would say that none of these are OK. You need to do the transform per-object and decide what to do with the rest. The most basic approach is:
func separateHashable(inputArray: [AnyObject]) -> (hashable: [AnyHashable], nonHashable: [AnyObject]) {
var hashableArray: [AnyHashable] = [AnyHashable]()
var nonHashableArray: [AnyObject] = [AnyObject]()
inputArray.forEach { item in
if let hashable = item as? AnyHashable {
hashableArray.append(item)
} else {
nonHashableArray.append(item)
}
}
return (hashableArray, nonHashableArray)
}
So this method will convert your array and split it into two arrays. You could then simply do let hashableArray: [AnyHashable] = separateHashable(inputArray: array).hashable.
But if you want to only extract those items that are hashable you can use convenience method on array compactMap
let hashableArray: [AnyHashable] = array.compactMap { $0 as? AnyHashable }
But in general this all very dangerous. And also that is why you can't simply convert it to Hashable. The reason for it is that 2 items may both be hashable but are of different class. And then both of the items may produce same hash even though they do not represent the same item. Imagine having 2 classes:
struct Question: Hashable {
let id: String
var hashValue: Int { return id.hashValue }
}
struct Answer: Hashable {
let id: String
var hashValue: Int { return id.hashValue }
}
Now if I create an array like this:
let array: [AnyObject] = [
Question(id: "1"),
Answer(id: "1")
]
both of the items would produce the same hash and one would potentially overwrite the other.

Swift: use filter function on array of dictionaries? Error: Cannot invoke 'filter' with an argument list of type

The goal of this code below is to filter out dictionaries with a certain ID, where ID is a string.
let dictArray = networkData["dicts"] as! [[String:AnyObject]]
localData["dicts"] = dictArray.filter{ ($0["id"] as! String) != sample.getId() }
This code, however, generates an error:
Cannot invoke 'filter' with an argument list of type '(([String :
AnyObject]) throws -> Bool)'
Based on other SO answers like this one and this one, it seems the error is the dictionaries don't conform to Equatable.
So is the only option for using filter to create a custom class to hold the array of dictionaries and make that class conform to Equatable?
If so, perhaps it seems cleaner to simply iterate and create a new array.
Filtering [[String:AnyObject]] (a.k.a. Array>) results in another [[String:AnyObject]]. You're trying to assign this to a var of type AnyObject, which is not allowed in Swift 3, since arrays are structs, not objects.
Make a type-safe struct or object to hold this data, rather than a dict.
For example:
let dictArray = networkData["dicts"] as! [[String:AnyObject]]
let filteredDicts = dictArray.filter{ ($0["id"] as! String) != sample.getId() }
localData["dicts"] = filteredDicts
The issue isn't on hash objects not conform to Equatable because you are using String to do the comparing.
I have the code runs well in Playground:
// make sure data is type of [String: [[String: AnyObject]]]
// make sure filteredData is type of [String: [[String: AnyObject]]]
let key = "hashes"
if let hashArray = data[key] {
let id = sample.getId() // make sure it's String type
filteredData[key] = hashArray.filter { ($0["id"] as? String) != id }
}

In Swift Convert [String: Any!] to URLComponents

I'm having a dictionary [String: Any!] that has values like integers, floats and strings. When I'm creating URLComponents using below code, its not taking values related to integers and floats.
func queryItems(dictionary: [String: Any]) -> [URLQueryItem] {
return dictionary.map {
URLQueryItem(name: $0, value: $1 as? String)
}
}
I think you should consider using [String:String] instead of [String:Any] and convert your values to String one step before sending it to queryItems function.But if you want to leave it like this than casting from value Int,Float,Double with as? String always fails.
you can use String().
Obviously Any could be anything. But if you have some control of your inputs one easy option is casting to LosslessStringConvertible. String, Substring and your basic numeric types all conform to it.
Warnings:
Bool returns true or false.
NSObject does not conform to this, so if you're mixing in NSNumber or NSString you need to account for those also.
func queryItems(dictionary: [String: Any]) -> [URLQueryItem] {
dictionary.map {
URLQueryItem(name: $0, value: ($1 as? LosslessStringConvertible)?.description)
}
}

Array from dictionary keys in swift

Trying to fill an array with strings from the keys in a dictionary in swift.
var componentArray: [String]
let dict = NSDictionary(contentsOfFile: NSBundle.mainBundle().pathForResource("Components", ofType: "plist")!)
componentArray = dict.allKeys
This returns an error of: 'AnyObject' not identical to string
Also tried
componentArray = dict.allKeys as String
but get: 'String' is not convertible to [String]
Swift 3 & Swift 4
componentArray = Array(dict.keys) // for Dictionary
componentArray = dict.allKeys // for NSDictionary
With Swift 3, Dictionary has a keys property. keys has the following declaration:
var keys: LazyMapCollection<Dictionary<Key, Value>, Key> { get }
A collection containing just the keys of the dictionary.
Note that LazyMapCollection that can easily be mapped to an Array with Array's init(_:) initializer.
From NSDictionary to [String]
The following iOS AppDelegate class snippet shows how to get an array of strings ([String]) using keys property from a NSDictionary:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let string = Bundle.main.path(forResource: "Components", ofType: "plist")!
if let dict = NSDictionary(contentsOfFile: string) as? [String : Int] {
let lazyMapCollection = dict.keys
let componentArray = Array(lazyMapCollection)
print(componentArray)
// prints: ["Car", "Boat"]
}
return true
}
From [String: Int] to [String]
In a more general way, the following Playground code shows how to get an array of strings ([String]) using keys property from a dictionary with string keys and integer values ([String: Int]):
let dictionary = ["Gabrielle": 49, "Bree": 32, "Susan": 12, "Lynette": 7]
let lazyMapCollection = dictionary.keys
let stringArray = Array(lazyMapCollection)
print(stringArray)
// prints: ["Bree", "Susan", "Lynette", "Gabrielle"]
From [Int: String] to [String]
The following Playground code shows how to get an array of strings ([String]) using keys property from a dictionary with integer keys and string values ([Int: String]):
let dictionary = [49: "Gabrielle", 32: "Bree", 12: "Susan", 7: "Lynette"]
let lazyMapCollection = dictionary.keys
let stringArray = Array(lazyMapCollection.map { String($0) })
// let stringArray = Array(lazyMapCollection).map { String($0) } // also works
print(stringArray)
// prints: ["32", "12", "7", "49"]
Array from dictionary keys in Swift
componentArray = [String] (dict.keys)
You can use dictionary.map like this:
let myKeys: [String] = myDictionary.map{String($0.key) }
The explanation:
Map iterates through the myDictionary and accepts each key and value pair as $0. From here you can get $0.key or $0.value. Inside the trailing closure {}, you can transform each element and return that element. Since you want $0 and you want it as a string then you convert using String($0.key). You collect the transformed elements to an array of strings.
dict.allKeys is not a String. It is a [String], exactly as the error message tells you (assuming, of course, that the keys are all strings; this is exactly what you are asserting when you say that).
So, either start by typing componentArray as [AnyObject], because that is how it is typed in the Cocoa API, or else, if you cast dict.allKeys, cast it to [String], because that is how you have typed componentArray.
extension Array {
public func toDictionary<Key: Hashable>(with selectKey: (Element) -> Key) -> [Key:Element] {
var dict = [Key:Element]()
for element in self {
dict[selectKey(element)] = element
}
return dict
}
}
dict.keys.sorted()
that gives [String]
https://developer.apple.com/documentation/swift/array/2945003-sorted
From the official Array Apple documentation:
init(_:) - Creates an array containing the elements of a sequence.
Declaration
Array.init<S>(_ s: S) where Element == S.Element, S : Sequence
Parameters
s - The sequence of elements to turn into an array.
Discussion
You can use this initializer to create an array from any other type that conforms to the Sequence protocol...You can also use this initializer to convert a complex sequence or collection type back to an array. For example, the keys property of a dictionary isn’t an array with its own storage, it’s a collection that maps its elements from the dictionary only when they’re accessed, saving the time and space needed to allocate an array. If you need to pass those keys to a method that takes an array, however, use this initializer to convert that list from its type of LazyMapCollection<Dictionary<String, Int>, Int> to a simple [String].
func cacheImagesWithNames(names: [String]) {
// custom image loading and caching
}
let namedHues: [String: Int] = ["Vermillion": 18, "Magenta": 302,
"Gold": 50, "Cerise": 320]
let colorNames = Array(namedHues.keys)
cacheImagesWithNames(colorNames)
print(colorNames)
// Prints "["Gold", "Cerise", "Magenta", "Vermillion"]"
Swift 5
var dict = ["key1":"Value1", "key2":"Value2"]
let k = dict.keys
var a: [String]()
a.append(contentsOf: k)
This works for me.
NSDictionary is Class(pass by reference)
Dictionary is Structure(pass by value)
====== Array from NSDictionary ======
NSDictionary has allKeys and allValues get properties with
type [Any].
let objesctNSDictionary =
NSDictionary.init(dictionary: ["BR": "Brazil", "GH": "Ghana", "JP": "Japan"])
let objectArrayOfAllKeys:Array = objesctNSDictionary.allKeys
let objectArrayOfAllValues:Array = objesctNSDictionary.allValues
print(objectArrayOfAllKeys)
print(objectArrayOfAllValues)
====== Array From Dictionary ======
Apple reference for Dictionary's keys and values properties.
let objectDictionary:Dictionary =
["BR": "Brazil", "GH": "Ghana", "JP": "Japan"]
let objectArrayOfAllKeys:Array = Array(objectDictionary.keys)
let objectArrayOfAllValues:Array = Array(objectDictionary.values)
print(objectArrayOfAllKeys)
print(objectArrayOfAllValues)
This answer will be for swift dictionary w/ String keys. Like this one below.
let dict: [String: Int] = ["hey": 1, "yo": 2, "sup": 3, "hello": 4, "whassup": 5]
Here's the extension I'll use.
extension Dictionary {
func allKeys() -> [String] {
guard self.keys.first is String else {
debugPrint("This function will not return other hashable types. (Only strings)")
return []
}
return self.flatMap { (anEntry) -> String? in
guard let temp = anEntry.key as? String else { return nil }
return temp }
}
}
And I'll get all the keys later using this.
let componentsArray = dict.allKeys()
// Old version (for history)
let keys = dictionary.keys.map { $0 }
let keys = dictionary?.keys.map { $0 } ?? [T]()
// New more explained version for our ducks
extension Dictionary {
var allKeys: [Dictionary.Key] {
return self.keys.map { $0 }
}
}

Resources