Array from dictionary keys in swift - ios

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 }
}
}

Related

Flatten an array of dictionaries to one dictionary

I am having an array of dictionaries with columnId and columnValue as a pair. Now i need to flatten it as columnId as the key and columnValue as the value of it. How is it possible to do with swift higher order functions?
let arrayOfDictionaries = [["columnId": 123, "columnValue": "sample text"], ["columnId": 124, "columnValue": 9977332]]
//The end result should be:
flattenedDictionary: [String: Any] = ["123": "sample text", "124": 9977332]
Note: Result dictionary will be in the form of [String: Any]
This would work:
func flatten(_ pairs: [[String: Any]]) -> [String: Any] {
pairs.reduce(into: [String: Any]()) {
if let id = $1["columnId"] as? Int, let value = $1["columnValue"] {
$0["\(id)"] = value
}
}
}
You can do this in two steps;
Convert your input array into a sequence of key-value pairs using compactMap
Convert the sequence back into a dictionary using Dictionary(uniqueKeysWithValues:)
let arrayOfDictionaries = [["columnId": 123, "columnValue": "sample text"], ["columnId": 124, "columnValue": 9977332]]
let tupleArray:[(String,Any)] = arrayOfDictionaries.compactMap { dict in
guard let id = dict["columnId"], let value = dict["columnValue"] else {
return nil
}
return ("\(id)",value)
}
let flattenedDictionary: [String: Any] = Dictionary(uniqueKeysWithValues: tupleArray)
Note that this code will throw an exception if there are duplicate keys. You should either take steps to ensure the columnId values are unique or use Dictionary(keysAndValues:, uniquingKeysWith:) to resolve id clashes.

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.

Mapping from array of dictionaries to array of numbers in Swift

Here is a function that should turn an an array of key-value pairs for orders NSArray containing Dictionary<String, Any> to an array of IDs for each order ([NSNumber]).
However, I am still having problem with the type conversion, the error:
Type 'Any' has no subscript members
How to perform the mapping cleanly in Swift?
#objc static func ordersLoaded(notification:Notification) -> [NSNumber] {
// Function receives a Notification object from Objective C
let userInfo:Dictionary = notification.userInfo as! Dictionary<String, Any>
// orders is an array of key-value pairs for each order Dictionary<String,Any>
let ordersWithKeyValuePairs:NSArray = userInfo["orders"] as! NSArray // Here a typed array of Dictionaries would be preferred
// it needs to be simplified to an array of IDs for each order (NSNumber)
// orderID is one of the keys
let orderIDs:[NSNumber];
orderIDs = ordersWithKeyValuePairs.flatMap({$0["orderID"] as? NSNumber}) // Line with the error
/*
orderIDs = ordersWithKeyValuePairs.map({
(key,value) in
if key==["orderID"] {
return value
} else {
return nil
}
}) as! [NSNumber]
*/
return orderIDs
}
You can try this
if let ordersWithKeyValuePairs = userInfo["orders"] as? [[String:Any]] {
let result = ordersWithKeyValuePairs.compactMap{$0["orderID"] as? Int }
}
Here is what worked, casting ordersWithKeyValuePairs to [Dictionary<String, Any>] solved the problem to me:
#objc static func ordersLoaded(notification:Notification) -> [NSNumber] {
// Function receives a Notification object from Objective C
let userInfo:Dictionary = notification.userInfo as! Dictionary<String, Any>
// orders is an array of key-value pairs for each order Dictionary<String,Any>
let ordersWithKeyValuePairs:[Dictionary<String, Any>] = userInfo["orders"] as! [Dictionary<String, Any>]
// it needs to be simplified to an array of IDs for each order (NSNumber)
// orderID is one of the keys
let orderIDs:[NSNumber];
orderIDs = ordersWithKeyValuePairs.flatMap({$0["orderID"] as? NSNumber}) // Line with the error
return orderIDs
}

Cannot subscript a value of type '[[String : Any]]' with an index of type 'String'

I'm trying to extract the info from a json array and i'm getting this error
"Cannot subscript a value of type '[[String : Any]]' with an index of type 'String'"
here
if let rev = place.details?["reviews"] as? [[String:Any]] {
if let ver = rev["author_name"] as? String { // <- IN THIS LINE I GET THE ERROR
}
}
i know that if i cast the type as [String : Any] instead of [[String:Any]] it will work, but in this case i have to cast it as an array of arrays otherwise it doesn't read the json, so how can i solve the problem?
[[String:Any]] is an array. It can be only subscripted by Int index.
You have to iterate over the array for example:
if let reviews = place.details?["reviews"] as? [[String:Any]] {
for review in reviews {
if let authorName = review["author_name"] as? String {
// do something with authorName
}
}
}
You can't access an item in an array with a String. You have to use Int
[[String:Any]] This is an array of dictionaries.
[[String:Any]] is a two dimensional Array. It can be only subscripted using Int index.
It is better to use a forEach loop, e.g.
if let reviews = place.details?["reviews"] as? [[String:Any]] {
reviews?.forEach { review in
if let authorName = review["author_name"] as? String {
// do something with authorName
}
}
}
I think you are mixing up dictionaries and arrays here.
If you want to access an element in an array, you have to use an Int index like this
let a = ["test", "some", "more"] // your array
let b = a[0] // print(b) = "test"
If you want to access an element in a dictionary, you can access it via its key, in your case a String
let dict: [String: Any] = ["aKey": "someValue"]
let value = dict["aKey"] // print(value) = "someValue"
In your case you have an array of dictionaries, each of them containing information about a review. If you want to access the author of one of your reviews, you have to first get the review dictionary out of your array like this:
if let reviews = place.details?["reviews"] as? [[String:Any]],
let review = reviews[0] {
// here you can access the author of the review then:
if let author = review["author_name"] as? String {
// do something
}
}
Instead of accessing just the first review like in my example, you can also loop via the array to access all of the reviews one by one

Swift filter dictionary error: Cannot assign a value of type '[(_, _)]' to a value of type '[_ : _]'

I am trying to filter a dictionary in swift:
var data: [String: String] = [:]
data = data.filter { $0.1 == "Test" }
the filter code above compiles under Swift 2 but yields the following error:
Cannot assign a value of type '[(String, String)]' to a value of type '[String : String]'
is this a bug in the Swift compiler or is this not the right way to filter dictionaries in Swift?
This has been fixed in Swift 4
let data = ["a": 0, "b": 42]
let filtered = data.filter { $0.value > 10 }
print(filtered) // ["b": 42]
In Swift 4, a filtered dictionary returns a dictionary.
Original answer for Swift 2 and 3
The problem is that data is a dictionary but the result of filter is an array, so the error message says that you can't assign the result of the latter to the former.
You could just create a new variable/constant for your resulting array:
let data: [String: String] = [:]
let filtered = data.filter { $0.1 == "Test" }
Here filtered is an array of tuples: [(String, String)].
Once filtered, you can recreate a new dictionary if this is what you need:
var newData = [String:String]()
for result in filtered {
newData[result.0] = result.1
}
If you decide not to use filter you could mutate your original dictionary or a copy of it:
var data = ["a":"Test", "b":"nope"]
for (key, value) in data {
if value != "Test" {
data.removeValueForKey(key)
}
}
print(data) // ["a": "Test"]
Note: in Swift 3, removeValueForKey has been renamed removeValue(forKey:), so in this example it becomes data.removeValue(forKey: key).
data.forEach { if $1 != "Test" { data[$0] = nil } }
Just another approach (a bit simplified) to filter out objects in your dictionary.
Per Apple docs, filter:
Returns an array containing, in order, the elements of the sequence that satisfy the given predicate.
https://developer.apple.com/reference/swift/sequence/1641239-filter
I found myself needing to do what the OP was asking about and ended up writing the following extensions (Swift 3):
extension Dictionary
{
func filteredDictionary(_ isIncluded: (Key, Value) -> Bool) -> Dictionary<Key, Value>
{
return self.filter(isIncluded).toDictionary(byTransforming: { $0 })
}
}
extension Array
{
func toDictionary<H:Hashable, T>(byTransforming transformer: (Element) -> (H, T)) -> Dictionary<H, T>
{
var result = Dictionary<H,T>()
self.forEach({ element in
let (key,value) = transformer(element)
result[key] = value
})
return result
}
}
Usage:
let data = ["a":"yes", "b":"nope", "c":"oui", "d":"nyet"]
let filtered = data.filteredDictionary({ $0.1 >= "o" })
// filtered will be a dictionary containing ["a": "yes", "c": "oui"]
I've found this method to be useful after filtering or applying some other transform that results in an array of dictionary elements:
extension Array {
func dictionary<K: Hashable, V>() -> [K: V] where Element == Dictionary<K, V>.Element {
var dictionary = [K: V]()
for element in self {
dictionary[element.key] = element.value
}
return dictionary
}
}
To use it, just say something like:
dictionary = dictionary.filter{ $0.key == "test" }.dictionary()
The advantage of this method is that no argument of any kind needs to be passed to the dictionary() method. Generic type arguments tell the compiler everything it needs to know.
You can arrange ascending order according to dictionary value using filter
let arrOfDict = [{"ABC":24},{"XYZ":21},{"AAA":23}]
let orderedDict = arrOfDict.filter{$0.value < $1.value}
you will get below output:
[
{ "XYZ": 21 },
{ "AAA": 23 },
{ "ABC": 24 }
]

Resources