swift3 - how do I access dictionary in an array? - swift

I want to make an array of several dictionaries for my world clock app like this:
let countriesList = [
["country": "Tokyo, Japan", "gmc": "GMT+9"],
["country": "New York, USA", "gmc": "GMT-5"]
]
Since I'm getting no error, I'm assuming this is okay.
Now my question is, how do I access each country?
I'm trying to list them all into a table, and I have googled but I got nothing. Can anyone help?

it's highly recommended to use a custom struct for that purpose:
struct Country {
let name, gmc : String
}
let countriesList = [
Country(name: "Tokyo, Japan", gmc: "GMT+9"),
Country(name: "New York", gmc: "GMT-5")
]
Now in cellForRow you can easily access the properties:
let country = countriesList[indexPath.row]
cell.textLabel?.text = country.name
cell.detailTextLabel?.text = country.gmc
No key subscription, no type casting, no optional binding, no problems ;-)
You can print all the names with
print(countryList.map{ $0.name })

In your cellForRowAt method where you are setting country to label.
cell.lblCountry.text = self.countriesList[indexPath.row]["country"] as? String
If you want all country list from your array of dictionary.
let countries = self.countriesList.flatMap { $0["country"] as? String }
With this you will get array of string with all country value.

You can use subscript:
for country in countriesList {
print(country["country"])
}

Related

SwiftUI, Firestore get array of object within object

I am working on a recipe-app connected to firestore and have trouble reading the data saved in the database. I save a recipe that consists of title, id etc but it also contains an array of ingredients. This array is a struct containing id, name and amount. I am able to get the recipe object but the array of ingredients is empty. This is how is get the recipe
private func listenForRecipes() {
db.collection("recipe").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.recipes = documents.map { queryDocumentSnapshot -> RecipePost in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let steps = data["steps"] as? [Step] ?? []
let ingredients = data["ingredients"] as? [Ingredient] ?? []
let serves = data["serves"] as? Int ?? 0
let author = data["author"] as? String ?? ""
let authorId = data["authorId"] as? String ?? ""
let category = data["category"] as? String ?? ""
let image = data["image"] as? String ?? ""
print("\(ingredients)")
return RecipePost(title: title, steps: steps, ingredients: ingredients, serves: serves, author: author, authorId: authorId, category: category, image: image)
}
}
}
Thankful for any help.
The data that you're getting from Firebase is coming back to you in the form of a [String:Any] dictionary. Your current code is taking those dictionary keys (title, author, etc) and doing optional casts (the as?), telling the system "if this data is actually a String, then set my variable to that value. If not (??), here's the default value to use instead"
The problem comes when you introduce custom types. The system doesn't inherently know that your item is an Ingredient or Step. So, the cast fails, and you get the default value of [].
You have two options:
Use a custom type for your entire document (see Firebase documentation on this here: https://firebase.google.com/docs/firestore/query-data/get-data#swift_3). This SO question is also relevant: How to convert document to a custom object in Swift 5?
Convert the [String:Any] dictionary (or array of dictionaries as it may be in this case) yourself. First step might be to print data["ingredients"] to the console to see what it really has in it. Without being able to see what you actually have in Firestore, Let's assume it is a [[String:Any]] (an array of dictionaries). Then your conversion might look like this:
let ingredients = (data["ingredients"] as? [[String:Any]]).map { item in
return Ingredient(id: item["id"] as? String ?? "", name: item["name"] as? String ?? "", amount: item["amount"] as? String ?? "")
}
You can also experiment with using Codable, which could allow you to automate some of this process, say with JSONDecoder to do some of the work for you. Relevant SO: How can I use Swift’s Codable to encode into a dictionary?

Swift - Array to Dictionary

So, I have the following objects:
struct Person {
let name: String
let birthday: String
}
let Bob = Person(name: "Bob", birthday: "11.12.1987")
let Tim = Person(name: "Tim", birthday: "11.12.1987")
let John = Person(name: "John", birthday: "01.02.1957")
let Jerry = Person(name: "Jerry", birthday: "17.12.2001")
And the following array:
let people = [Bob, Tim, John, Jerry]
My goal is to generate a dictionary from this array, with "birthday" for the key and the "Person" object itself as the value: [String: [Person]]. If there are equal keys the person should be added and form an array as a key. So the result will be the following:
dictionary = ["11.12.1987": [Bob, Tim], "01.02.1957": John, "17.12.2001": Jerry]
What is the best way to achieve this?
Cheers!
You could use Dictionary(grouping:by:) method together with mapValues,
let result = Dictionary(grouping: people, by: \.birthday)
print(result)
Output is,
["01.02.1957": ["John"], "17.12.2001": ["Jerry"], "11.12.1987":
["Bob", "Tim"]]
This would yield you a Dictionary of type Dictionary<String, [Person]>.
From, your question it seems like you want a Person if there is only one Person and array when there are multiple people. You would loose type in that situation and your type would be something like this, Dictionary<String, Any>. You could make small bit of modification to the above code to do that.
let result = Dictionary(grouping: people, by: \.birthday)
.mapValues { people -> Any in
if people.count == 1 {
return people.first!
}
return people
}
print(result)
The output would be similar to what you want,
["01.02.1957": "John", "17.12.2001": "Jerry", "11.12.1987": ["Bob",
"Tim"]]
What about a loop through the dictionary checking if date exists. Otherwise create key with date and assign value.
If key already exists append person to the actual key value:
var dictionary = [String: [Person]]()
// Loop through all person objects in array
for person in people {
// Check if date already exists
if dictionary[person.birthday] != nil {
// If already exists, include new person in array
var array = dictionary[person.birthday]
array!.append(person)
}
// If date does not exists, add new key with date and person
else {
dictionary[person.birthday] = [person]
}
}

"Unwrapping" data retrieved from Firebase

So I have managed to retrieve some data from Firebase and it looks like this when printed:
[Resturant.CustomerList(key: "-LQQlhEmNZb8Kaha9uCk", customerLastName:
“Kendrick”, customerFirstName: “Anna”, customerSeat: "100",
customerOrder: “Noodle”, Timestamp: 1541290545703.0)]
Question: How do I unwrap them so I can put individual value into other String variables?
I tried many ways but I get errors such cannot subscript a value of type [CustomerList] with an index of type String if I do something like let custName = self.list["Name"] as? String
ps. CustomerList is a struct
pps. The print out of list is what is shown
As you have a list of CustomerList objects i.e, [CustomerList] so you should first get a single object from this list. Lets say we want the very first object from this list to access its properties then we can do it as below,
if let firstCustomer = self.list.first {
let firstName = firstCustomer.customerFirstName
let lastName = firstCustomer.customerLastName
}
If you want to access an object at a specific index then you can do as below,
let index = 0
let customer = self.list[index]
let firstName = customer.customerFirstName
let lastName = customer.customerLastName
To find a particular customer, you can filter that as below,
let johny = self.list.filter{ $0.customerFirstName == "Jonhny"}.first {
print(johny.customerLastName)
}
To get a custom list created from the customers list, you can use map as below,
let lastNamesArray = self.list.map({ $0.customerLastName })

Realm query problem with sorted localized data

Consider the following Realm models:
class Fruit: Object {
#objc dynamic var name = ""
let localizations = List<Localization>()
/**
Returns the localized name of the fruit matching the preferred language of the app
or self.name if the fruit does not have a localization matching the user preferred language codes.
*/
var localizedName: String? {
guard !Locale.isPreferredLanguageDefaultAppLanguage else { return self.name }
let preferredLanguagesCodes = Locale.preferredLanguagesCodes
let localizations = preferredLanguagesCodes.compactMap({ languageCode in Array(self.localizations).filter({ $0.languageCode == languageCode }).first })
return localizations.first?.localizedName ?? self.name
}
}
class Localization: Object {
#objc dynamic var localizedName: String = ""
#objc dynamic var languageCode: String = ""
}
Let's say I have 2 fruits in my database (represented in JSON format for the sake of simplicity):
[
{
"name": "Apple",
"localizations": [
{
"localizedName": "Pomme",
"languageCode": "fr"
}
]
},
{
"name": "Banana",
"localizations": [
{
"localizedName": "Banane",
"languageCode": "fr"
}
]
}
]
Now I want to get all the fruits in my database, and sort them alphabetically by their localizedName.
var localizedNameSortingBlock: ((Fruit, Fruit) -> Bool) = {
guard let localizedNameA = $0.localizedName, let localizedNameB = $1.localizedName else { return false }
return localizedNameA.diacriticInsensitive < localizedNameB.diacriticInsensitive
}
let sortedFruits = Array(Realm().objects(Fruit.self)).sorted(by: localizedNameSortingBlock)
If the first preferred language of my device is "English", I get this:
Apple
Banana
If it's set to "French":
Banane
Pomme
It's quite simple, but this solution has a major inconvenient:
By casting the Results<Fruit> collection into an array, I'm loosing the ability to get live updates via Realm's notification token system.
The problem is, I can't sort using NSPredicate directly on the Results collection, because the localizedName property is a computed property, thus is ignored by Realm.
I thought about writing the localizedName value directly into the name property of the Fruit object, but doing so requires to loop through all fruits and change their name whenever the user change of preferred language. There must be a better way.
So my question is:
Is there a way to retrieve all the fruits in my database, get them sorted by their localizedName, without loosing the ability to receive batch updates from Realm?

Adding additional items to dictionary

Would like to add John together with Peter in this combination:
var myData0: [String: String] = ["Item": "Milk", "Description": "Milk is white", "DrinksMilk": "Peter"]
myData0["DrinksMilk"] = "John"
println(myData0)
Println gives only John back instead Peter AND John. Is there a way to add John without overwriting Peter?
If you want to keep a string value of dictionary, you can do like that
var myData0: [String: String] = ["Item": "Milk", "Description": "Milk is white", "DrinksMilk": "Peter"]
myData0["DrinksMilk"] = myData0["DrinksMilk"]! + ", John"
println(myData0)
If not, you can change the type of dictionary value to AnyObject like that :
var myData0: [String: AnyObject] = ["Item": "Milk", "Description": "Milk is white", "DrinksMilk": "Peter"]
myData0["DrinksMilk"] = [myData0["DrinksMilk"]!, "John"]
println(myData0)
It appears that you are attempting to encode a data type into a dictionary. A better approach, not knowing anything more about your specific problem, is to define a class for this:
class Drink {
var name:String
var desc:String
var drinkers:[Person]
// ...
func addDrinker (person:Person) {
drinkers.append(person)
}
}
You've declared your dictionary to hold values of type String but, as you describe, now you want the dictionary values to be String or Array. The common type of these type value types is Any (it can be AnyObject if you've imported Foundation; but that is a Swift anomaly). You will then need to 'fight' the type system to coerce the "DrinksMilk" field as a modifiable array and to add "John".
// First extract `"DrinksMilk"` and force it to a mutating array:
var drinkers:[String] = myData0["DrinksMilk"]! as [String]
// Add the new drinker
drinkers.append("John")
// Reassign back into data
myData0["DrinksMilk"] = drinkers