SwiftUI, Firestore get array of object within object - swift

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?

Related

Why am I receiving a "Cast from '[String]' to unrelated type 'String' always fails" error?

I’m trying to display an array of data (stored within a Firestore database field entitled "product") into a UITableViewController. The code I am currently using to try and return the array into my app is below:
if userId == user?.uid {
let group_array = document["product"] as? Array ?? [""]
let productName1 = (group_array) as? String
self.tableViewData =[cellData(opened: false, title: "Item 1", sectionData: [productName1 ?? "No data to display :("])]
}
When I run this, it compiles, however I’m currently getting the following warning message:
Cast from '[String]' to unrelated type 'String' always fails”
This results in no data being returned, and returning nil for productName1, consequently leading to the "No data to display :(" message being shown instead of the data.
Does anyone know which type I would need to assign for the following line to return this data please?
let productName1 = (group_array) as? String
You've defined group_array as an Array. Then you try to cast it to a String. You can't cast an Array (even an array of String objects) to a String.
This line:
let productName1 = (group_array) as? String
Will always fail.
What result are you looking for? The first entry in the array of strings? A string that combines all the elements in the array together?
If you want the first element, use
let productName1 = group_array.first as? string ?? ""
If you want to combine the items, use something like this:
let productNames = (group_array as? [String])?.joined(separator: ", ") ?? ""
By the way, your code would be cleaner if you cast your array to an array of Strings, not just a generic Array (Array of Any)
Change:
let group_array = document["product"] as? Array ?? [""]
To:
let group_array = document["product"] as? [String] ?? [""]

Firebase-Swift How to sum a child values together

I need to retrieve price value of each product in the cart child, but how should I retrieve it and sum the retrieved value together?
Picture of my Firebase database structure
let uid = Auth.auth().currentUser?.uid
refProduct = Database.database().reference().child("users").child(uid!).child("cart")
refProduct.observeSingleEvent(of: .value) { (snapshot) in
for cartchild in snapshot.children{
let snap = cartchild as! DataSnapshot
let key = snap.value
.....
}
}
I would not store the price as a string, but as a number. You might want to add another field with currency if needed.
guard let uid = Auth.auth().currentUser?.uid else { return }
var sum: Double = 0
refProduct = Database.database().reference().child("users").child(uid).child("cart")
refProduct.observeSingleEvent(of: .value) { (snapshot) in
for cartchild in snapshot.children{
let snap = cartchild as! DataSnapshot
let data = snap.value as? [String: Any]
let price = data["ProductPrice"] as? Double ?? 0
sum += price
}
print("Final sum: \(sum)")
}
Not really tested, but this is the idea
Arvidurs is correct about storing the price as an int and the currency as a string, but the reason the answer isn't working for you is that it doesn't address that you're not correctly retrieving the data you want in the first place.
You have your cart folder, and it contains two product folders whose properties you're trying to retrieve. You can't retrieve and unwrap the values contained in those two folders by just referencing the parent cart folder. You need to individually access each folder within cart:
Database.database().reference().child("users").child(uid).child("cart").child("-Lf59bkQ5X3ivD6ue1SA")
Database.database().reference().child("users").child(uid).child("cart").child("-Lf5MiEGU357HWTMbxv8")
However, for this to work, you'll need access to each products autoID value, so you'll need to be storing each new product's childByAutoID value into an array or a dictionary so that you have them all available to access whatever data you need.
You'll need to implement this as you're storing the new product to the cart folder. I don't know exactly how you're currently saving each product, but you'll need to do something like this when you create your reference that you'll be saving to:
let newProductRef = Database.database().reference().child("users").child(uid).child("cart").childByAutoId()
let autoID = newProductRef.key
At that point, you'll be able to store autoID however you choose, and you'll have access to everything within the cart folder, and you can loop through all of your autoIDs and get whatever data you need. Example:
func getCartPriceSum(finished: #escaping ([String : Double]) -> Void){
let myGroup = DispatchGroup()
var sum = Double()
var currency = String()
for autoID in autoIdArray{
myGroup.enter()
let productRef = Database.database().reference().child("users").child(uid).child("cart").child(autoID)
productRef.observe(.value) { (snapshot) in
guard
let snapshotValue = snapshot.value as? NSDictionary,
let productPrice = snapshotValue["ProductPrice"] as? Double,
let priceCurrency = snapshotValue["PriceCurrency"] as? String//assuming you've adopted Arvidurs' method of storing the price data
else {
print("productPrice/priceCurreny nil")
return
}
sum += productPrice
currency = priceCurrency
}
myGroup.leave()
}
let priceSum = [currency : sum]
myGroup.notify(queue: .main) {
finished(priceSum)
}
}
And you could call the function like this:
getCartPriceSum { (priceSum) in
//do whatever you want with the priceSum
}
The only thing left for you to figure out is how you want to store those autoIDs.

Jumping into swift - trouble with array of dictionary from json

json comes in and is an array of dictionary:
let dict = try JSONSerialization.dictionary(data: data, options: .allowFragments)
(key: contact_383348580, value: {
email = "r#c.com";
"first_name" = Jon;
"last_name" = B;
tags = "";
})
(key: contact_445575065, value: {
email = "n.w#s.com";
"first_name" = "<null>";
"last_name" = "<null>";
tags = "";
})
Trying to map this to a User class (and then sort alpha by first name(?)) and then populate a tableview.
I'm all obj-c but trying to add this feature in my objc project through swift to improve my skills incrementally. But googling returns all different ways with different swift versions that don't seem to work (or frankly make sense to me anyway but I'll get there).
Currently I can create users but the names are empty.
for objects in dict {
print(objects)
let first = dict["first_name"] as? String
let last = dict["last_name"] as? String
let name = "\(first ?? "asas") \(last ?? "sdasd")"
let object = User(username: name)
contacts.append(object!)
}
print(" contacts \(contacts)")
You say you have an array of dictionaries and if that is correct then you have fooled yourself with the naming of your variables because dict is an array and objects a dictionary. I assume your code doesn't compile?
What about this
let array = try JSONSerialization.dictionary(data: data, options: .allowFragments)
for dict in array {
let first = dict["first_name"] as? String ?? "asas"
let last = dict["last_name"] as? String ?? "sdasd"
let name = "\(first) \(last)"
let object = User(username: name)
contacts.append(object)
}

Reading Firebase child by auto ID swift

This is a quick output of my Firebase tree, id like to access people_on_this_planit / userID node. My only issue is that node is within an auto ID; please any help would be appreciated. Thanks
planits
-LEmgxuG_13KNA5inRaB
Planit Title: "Some title"
people_on_planit
-LEmh6IxqguVBJEwghZv (auto ID)
userID: "kkdiEW0D9"
senderId: "39FdLfdIO8832"
Right now the code i am using is as follows, but i am getting nil when i print peopleonplanit.
ref = Database.database().reference()
let userID = Auth.auth().currentUser?.uid
let planitsRef = ref.child("planits")
for child in snapshot.children {
let childSnap = child as! DataSnapshot
let dict = childSnap.value as! NSDictionary
let planKey = childSnap.key
//print(planKey)
print(dict)
let title = dict["Planit Title"] as! String
let senderID = dict["senderId"] as! String
let peopleOnPlanit = dict["people_on_planit"] as? String
print(peopleOnPlanit)
}
Here's the code to read the Firebase structure proposed in the question
let planitsRef = self.ref.child("planits")
planitsRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let planSnap = child as! DataSnapshot
let planDict = planSnap.value as! [String: Any]
let title = planDict["plan_title"] as! String
let senderId = planDict["sender_id"] as! String
print("plan title: \(title) senderId: \(senderId)")
let peopleOnPlanitSnap = planSnap.childSnapshot(forPath: "people_on_planit")
for peopleChild in peopleOnPlanitSnap.children {
let peopleChildSnap = peopleChild as! DataSnapshot
let userSnapDict = peopleChildSnap.value as! [String: Any]
let userId = userSnapDict["user_id"] as! String
print(" userId: \(userId)")
}
}
})
and the output
plan title: Some title senderId: 39FdLfdIO8832
userId: uid_0
userId: uid_1
-- A couple of notes --
Since the associated users appear to be stored by their user Id and that's the only data stored within that node, you may want to change this node
people_on_planit
-LEmh6IxqguVBJEwghZv (auto ID)
userID: "kkdiEW0D9"
to look like this
people_on_planit
uid_x: true //uid_x would be kkdiEW0D9 etc
uid_y: true
it's a bit shallower and cleaner and requires less code to read it. Also, you can query it more easily if that ever comes up.
also note that I changed the naming convention slightly; you are welcome to format keys however you want but I went with keys like plan_title instead of PlanIt Title and sender_id and user_id instead of senderId and userId. My practice is for actual firebase keys I use all lowercase, underscore instead of spaces, and in code, I use lower/uppercase with no spaces. So if you copy paste my code, you'll need to change those.

'(key: AnyObject, value: AnyObject)' does not have a member named 'subscript'

I have a problem similar to this question, but the answers there isn't helping me.
I have these lines of code:
var id = item["id"] as? String ?? ""
var name = item["name"] as? String ?? ""
var pic = item["pic"] as? String ?? ""
To me these lines of code a pretty much the same. For Xcode, this is a different matter.
The first line is fine. The second two lines generate this error:
'(key: AnyObject, value: AnyObject)' does not have a member named 'subscript'
Here is some more context for you all:
class func getFromJson(json:NSDictionary) -> [Collection] {
var collections = [Collection]()
if json.count > 0 {
for item in json {
var id = item["id"] as? String ?? ""
var name = item["name"] as? String ?? ""
var pic = item["pic"] as? String ?? ""
var newUser = Collection(id:id, name:name, pic:pic)
collections.append(newUser)
}
}
return collections
}
Can anyone explain to me how to fix this. Bonus points if you can explain why the first line is fine, but the next two nearly identical lines produce errors!
Thanks in advance.
'(key: AnyObject, value: AnyObject)' indicates that item is not an Dictionary but is a Tuple with a single key/value pair.
Iterating dictionaries in swift interates through tuples:
for (key, value) in json {
println(key, value)
}
Your for loop indicates that you are probably wanting a json Array of tuples instead of a json Dictionary.
item["id"] would give you a compile time error if you declared the parameter as a tuple. It seems you stumbled onto something hidden with the language with how either tuples or subscripts work under the hood.
More on Subscripts
More on Types (Tuples)