My project contains an array of MealPlans. Right now, a user can star a MealPlan and that will update the isStarred Bool value in the MealPlan document. However, this just updates the database that every user currently accesses.
How can I code it so that a user has their own personal set of isStarred MealPlans?
I'm currently using Firebase Authentication and Firestore. This is my MealPlan struct:
struct MealPlan {
var docID:String?
var title:String?
var recipeSource:String?
var estimatedTime:String?
var coverImageView:String?
var equipment:[String]?
var instructions:[String]?
var isStarred:Bool?
}
My User struct:
struct User {
var userId:String?
var firstName:String?
var lastName:String?
var email:String?
}
My data model:
class MealPlanModel {
var delegate:MealPlanProtocol?
var listener:ListenerRegistration?
func getMealPlans(_ starredOnly:Bool = false) {
// Detach any listener
listener?.remove()
// Get a reference to the database
let db = Firestore.firestore()
var query:Query = db.collection("mealPlans")
// If filtering for starred Meal Plans, update the query
if starredOnly {
query = query.whereField("isStarred", isEqualTo: true)
}
self.listener = query.addSnapshotListener({ (snapshot, error) in
// Check for errors
if error == nil && snapshot != nil {
var mealPlans = [MealPlan]()
// Parse documents into mealPlans
for doc in snapshot!.documents {
let m = MealPlan(
docID: doc["docID"] as? String,
title: doc["title"] as! String,
recipeSource: doc["recipeSource"] as? String,
estimatedTime: doc["estimatedTime"] as? String,
coverImageView: doc["coverImageView"] as? String,
ingredientsProduce: doc["ingredientsProduce"] as? [String],
ingredientsProtein: doc["ingredientsProtein"] as? [String],
ingredientsSpices: doc["ingredientsSpices"] as? [String],
ingredientsOther: doc["ingredientsOther"] as? [String],
equipment: doc["equipment"] as? [String], instructions: doc["instructions"] as? [String],
isStarred: doc["isStarred"] as? Bool)
mealPlans.append(m)
}
// Call the delegate and pass back the notes in the main thread
DispatchQueue.main.async {
self.delegate?.mealPlansRetrieved(mealPlans: mealPlans)
}
}
})
}
func updateStarredStatus(_ docId:String, _ isStarred:Bool) {
let db = Firestore.firestore()
db.collection("mealPlans").document(docId).updateData(["isStarred":isStarred])
}
}
And the method for starring in my View Controller:
#IBAction func starButtonTapped(_ sender: Any) {
// Toggle the star filter status
isStarFiltered.toggle()
// Run the query
if isStarFiltered {
model.getMealPlans(true)
}
else {
model.getMealPlans()
}
// Update the starButton
setStarFilterButton()
}
Would it involve copying the docID of a starred MealPlan into a key in the Users struct? And then displaying those MealPlans when filtering for starred MealPlans?
Any help/guidance is much appreciated!
A solution is for each user to track their own meal plans. You could create a collection that stores the meal plan id's within the users document so it would look like this
meals
meal_0 //auto generated documentId
//meal ingredients
meal_1
//meal ingredients
meal_2
//meal ingredients
and then
users
user_0 //the documentId is the users uid
//user info
my_meals (collection)
meal_0: true
mean_2: true
That being said, it's often a good idea to keep data at a high level to reduce the amount of reads and simplify queries so this structure may be better instead of storing the users meals within the users document
starred_meals
user_0 //the documentId is the users uid
meal_0: true
mean_2: true
When a use logs in, read their document from the starred_meals collection.
To expand on that a little bit, sometimes a user will want to store other information with their meal so instead the above, how about this
starred_meals
user_0
starred_meal_0 (auto generated documentId)
meal_id: meal_0
wine_pairing: "Cabernet Sauvignon"
starred_meal_1
meal_id: meal_2
wine_pairing: "Barolo"
with that you could store all kinds of other information associated with each meal, per user
Related
Hi there,
Ive been coding an app for my friend and me recently and currently I'm implementing Google Firebase's Firestore Database. I have set up a Data Model and a View Model to handle data to my view. Bear in mind I'm still new to Swift(UI) so my code might be a little messy.
This is where the database is accessed and the data is put into the data model.
Friends_Model.swift
import Foundation
import Firebase
import FirebaseFirestore
class Friends_Model: ObservableObject {
#Published var friend_list = [Friends_Data]()
#Published var noFriends = false
func getData() {
let db = Firestore.firestore()
db.collection("users").getDocuments { snapshot, error in
//check for errors
if error == nil {
print("no errors")
if let snapshot = snapshot {
//Update the list property in main thread
DispatchQueue.main.async {
//get all docs and create friend list
self.friend_list = snapshot.documents.map { d in
//Create friend item for each document
return Friends_Data(id: d.documentID,
userID: d["userID"] as? String ?? "")
}
}
}
} else {
// handle error
}
}
}
}
This is my data model. To my understanding this just sets the variables.
Friends_Data.swift
import Foundation
struct Friends_Data: Identifiable {
var id: String
var userID: String
}
This is my actual view where I output the data (just the relevant part ofc).
FriendsPanel.swift (Swift View File)
// var body etc. etc.
if let user = user {
let uid = user.uid ?? "error: uid"
let email = user.email ?? "error: email"
let displayName = user.displayName
VStack {
Group{
Text("Your Friends")
.font(.title)
.fontWeight(.bold)
}
List (friends_model.friend_list) { item in
Text(item.userID)
}
.refreshable {
friends_model.getData()
}
}
// further code
Displaying all entries in the database works fine, though I'd wish to only display the entries with the attribute "friendsWith" having the same string as oneself (uid).
Something like
if friends_model.friends_list.userID == uid {
// display List
} else {
Text("You don't have any friends")
}
I couldn't work it out yet, although I've been going on and about for the past 2 hours now trying to solve this. Any help would be greatly appreciated. Also sorry if I forgot to add anything.
Load only the data you need:
Use a query:
let queryRef = db.collection("users").whereField("friendsWith", isEqualTo: uid)
and then:
queryRef.getDocuments { snapshot, error in......
Here you can find more about firestore:
https://firebase.google.com/docs/firestore/query-data/queries
You need to make a View that you init with friends_model.friend_list and store it in a let friendList. In that View you need an onChange(of: friendList) and then filter the list and set it on an #State var filteredFriendList. Then in the same view just do your List(filteredFriendList) { friend in
e.g.
struct FiltererdFriendView: View {
let friendList: [Friend] // body runs when this is different from prev init.
#State var filteredFriendList = [Friend]()
// this body will run whenever a new friendList is supplied to init, e.g. after getData was called by a parent View and the parent body runs.
var body: some View {
List(filteredFriendList) { friend in
...
}
.onChange(of: friendList) { fl in
// in your case this will be called every time the body is run but if you took another param to init that changed then body would run but this won't.
filteredFriendList = fl.filter ...
}
}
}
This is the structure of my Firestore data right now:
I'm having trouble just accessing the favourites array. I seem to be accessing the collection and document just fine, but I can't seem to pull the favourites array in my code.
This is the relevant code in my data model
protocol UserFavouritesProtocol {
func userFavouritesRetrieved(userFavourites:[UserFavourites])
}
var delegateTest:UserFavouritesProtocol?
func getFavourites() {
// Detach any listener
listener?.remove()
// Get a reference to the database
let db = Firestore.firestore()
var query:Query = db.collection("userFavourites")
self.listener = query.addSnapshotListener({ (snapshot, error) in
// Check for errors
if error == nil && snapshot != nil {
var userFavourites = [UserFavourites]()
// Parse documents into mealPlans
for doc in snapshot!.documents {
let u = UserFavourites(
favouriteMealPlans: doc["favourites"] as? [String])
userFavourites.append(u)
}
// Call the delegate and pass back the notes in the main thread
DispatchQueue.main.async {
self.delegateTest?.userFavouritesRetrieved(userFavourites: userFavourites)
}
}
})
}
This is my UserFavourites struct:
struct UserFavourites {
var favouriteMealPlans:[String]?
}
At this point, how can I access the favourites array and call a reference to it? For example, I later want to see if a particular string is contained within my favourites array.
This will get you closer to your solution...
// struct
import FirebaseFirestore
struct UserFavourites {
var favouriteMealPlans:[DocumentReference]?
}
// usage
let u = UserFavourites(favouriteMealPlans: doc["favourites"] as? [DocumentReference])
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX") inside my "Wishlists" - collection.
I tried this but that's not allowed:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").document(list.name).collection("wünsche").getDocuments()
This is my function:
func getWishes (){
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
var counter = 0
for list in self.dataSourceArray {
print(list.name) // -> right order
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments() { ( querySnapshot, error) in
print(list.name) // wrong order
if let error = error {
print(error.localizedDescription)
}else{
// DMAG - create a new Wish array
var wList: [Wish] = [Wish]()
for document in querySnapshot!.documents {
let documentData = document.data()
let wishName = documentData["name"]
wList.append(Wish(withWishName: wishName as! String, checked: false))
}
// DMAG - set the array of wishes to the userWishListData
self.dataSourceArray[counter].wishData = wList
counter += 1
}
}
}
}
This is what I actually would like to achieve in the end:
self.dataSourceArray[ListIDX].wishData = wList
Update
I also have a function that retrieves my wishlists in the right order. Maybe I can add getWishesin there so it is in the right order as well.
func retrieveUserDataFromDB() -> Void {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
db.collection("users").document(userID).collection("wishlists").order(by: "listIDX").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
}else {
// get all documents from "wishlists"-collection and save attributes
for document in querySnapshot!.documents {
let documentData = document.data()
let listName = documentData["name"]
let listImageIDX = documentData["imageIDX"]
// if-case for Main Wishlist
if listImageIDX as? Int == nil {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: UIImage(named: "iconRoundedImage")!, wishData: [Wish]()))
// set the drop down menu's options
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(UIImage(named: "iconRoundedImage")!)
}else {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: self.images[listImageIDX as! Int], wishData: [Wish]()))
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(self.images[listImageIDX as! Int])
}
// // create an empty wishlist
// wList = [Wish]()
// self.userWishListData.append(wList)
// reload collectionView and tableView
self.theCollectionView.reloadData()
self.dropDownButton.dropView.tableView.reloadData()
}
}
self.getWishes()
}
For a better understanding:
git repo
As #algrid says, there is no sense to order the collection using order() if you are going to get an specific element using list.name at the end, not the first or the last. I would suggest to change your code to:
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments()
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX")
The following line of code will definitely help you achieve that:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").getDocuments() {/* ... */}
Adding another .document(list.name) call after .order(by: "ListIDX") is not allowed because this function returns a Firestore Query object and there is no way you can chain such a function since it does not exist in that class.
Furthermore, Firestore queries are shallow, meaning that they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a sub-collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the most simple solution I can think of would be to use two different queries and merge the results client-side. The first one would be the above query which returns a list of "wishlists" and the second one would be a query that can help you get all wishes that exist within each wishlist object in wünsche subcollection.
I solved the problem. I added another attribute when saving a wish that tracks the index of the list it is being added to. Maybe not the smoothest way but it works. Thanks for all the help :)
I'm quite new on Firestore and Firebase libraries. Can someone explain how could I retrieve the fields from my object and stored in a variable on my code? I'm trying to access all the latMin, latMax, lonMin, and lonMax from each object in my Firestore, as well if is a way to retrieve each field with the same name (ex. latMin from place 1 and 2). I don't know if this is possible or maybe there is a better way to have organized my Firebase.
Here is my Cloud Firebase: Here is the image for my Firestore, requested in the comments.
place 1: //This is an Object in my Firestore
//Field = Number : Value ----> This is how object is orginazed
latMax : 39.727
latMin : 39.726
lonMax : -121.7997
lonMin : -121.8003
place 2:
latMax : 39.7559
latMin : 39.755
lonMax : -122.1988
lonMin : -122.1992
I was able to access the objects without any problem, this is the function I'm using to read the documents on my Firestore:
import UIKit
import FirebaseFirestore
import Firebase
import CoreLocation
class SecondViewController: UIViewController, CLLocationManagerDelegate {
//Creating access to locationManager
var locationManager : CLLocationManager!
#IBOutlet weak var latLabel: UILabel!
#IBOutlet weak var lonLabel: UILabel!
#IBOutlet weak var place: UILabel!
#IBOutlet weak var placeImage: UIImageView!
//Storing the pass data that we got from the firt View
var lonStore = String()
var latStore = String()
var fireLon = String()
var fireLat = String()
var lonNumberStore = Double()
var latNumberStore = Double()
var fireLonMax = Double()
var fireLatMax = Double()
var fireLonMin = Double()
var fireLatMin = Double()
override func viewDidLoad() {
//Here goes some code to display on the SecondViewController
readArray()
}
func readArray() {
let placeRef = Firestore.firestore().collection("places")
placeRef.getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error \(error!)")
return
}
for document in snapshot.documents {
let documentId = document.documentID
let latMax = document.get("latMax") as! String //Getting a nil error
print(documentId, latMax) //This print all objects
}
}
}
I'm missing something and I know it, the problem is that I can't find any reference of what I need to do in this case, I had read the documentation like 50 times and I can't find the solution. Thanks for taking the time to read my post.
Your readArray code is SUPER close. Just need to add code to read the individual fields within the document
func readArray()
self.db.collection("places").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let docId = document.documentID
let latMax = document.get("latMax") as! String
let latMin = document.get("latMin") as! String
let lonMax = document.get("lonMax") as! String
let lonMin = document.get("lonMin") as! String
print(docId, latMax, latMin, lonMax, lonMin)
}
}
}
The problem in the original question is the structure of the database. Here's a structure that will match the code in my answer and will be better suited for what the OP wants to accomplish.
The structure in the question has multiple places listed under one collection - that's OK but it groups the places together. If we change that to have one place per collection, it makes iterating over them and getting to the data much easier.
I think your places model have references to two objects bestBuy and house. So, the retrieval approach would be to retrieve the data from and store in the object only. Means you can directly set all the data in plcaes model. Then you can call getter methods of bestBuy and house to retrieve the data as you want. Here is a sampe code (here it is retrieving only one document) but you can apply the same for all documents by iterating a for loop and converting each document to object:-
let docRef = db.collection("cities").document("BJ")
docRef.getDocument { (document, error) in
if let city = document.flatMap({
$0.data().flatMap({ (data) in
return City(dictionary: data)
})
}) {
print("City: \(city)")
} else {
print("Document does not exist")
}
}
But according to firestore documentation the best way to store nested objects is creating a subcollection and then storing it in its document.
Some clarification since the iOS Firestore documentation is, IMO, light in many areas: there are two ways to retrieve the values from a document field (once the document has been gotten)--using a Firestore method (per the documentation) and using Swift's native method of getting values from dictionaries.
let query = Firestore.firestore().collection("zipCodes").whereField("california", arrayContains: 90210)
query.getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
error == nil else {
return print("error: \(error!)")
}
guard !snapshot.isEmpty else {
return print("results: 0")
}
for doc in snapshot.documents {
// using firestore's convention
if let array = doc.get("zipCodes") as? [Int] {
if array.contains(90211) {
// doc contains both zip codes
}
}
// using swift's convention
if let array = doc.data()["zipCodes"] as? [Int] {
if array.contains(90211) {
// doc contains both zip codes
}
}
}
}
Personally, I would use as much of Firestore's framework as possible for safety and predictability and, therefore, use get() for all field retrieval.
I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}