Getting query of documents that another document is referencing - swift

I'm a beginner to Swift and Firebase and I'm creating an app where users can create events and attend events that other users create so I have 2 collections in my Firebase database (users and posts as seen in the image)
Image
When a user joins an event the reference to that event is added to the list "eventsAttending" indicating that they are attending those events. Now I want to fetch the events / posts that are in this eventsAttending array and return them however I'm not quite sure how to do so. To fetch all posts of the same author I did the following
func fetchCurrentUsersPosts() async throws -> [Post] {
return try await fetchPosts(from: postsReference.whereField("author.id", isEqualTo: user.id))
}
Where fetchPosts(from:) is:
func fetchPosts(from query: Query) async throws -> [Post] {
let posts = try await query.getDocuments(as: Post.self)
return posts
}
And getDocuments(as:) is:
extension Query {
func getDocuments<T: Decodable>(as type: T.Type) async throws -> [T] {
let snapshot = try await getDocuments()
return snapshot.documents.compactMap { document in
try! document.data(as: type)
}
}
User model:
struct User: Identifiable, Codable, Equatable {
// unique id of user
var id: String
// name of user
var name: String
// email of user
var email: String
// birthday of user
var birthday: Date
// phone number of user
var phoneNumber: String
// posts that the user has made
var usersPosts: [Post]
// events that the user is attending
var eventsAttending: [Post]
}
So I want a way to fetch the posts stored in the eventsAttending array using these 2 functions if possible.

Related

What is the best way to store a ToDo on Firestore?

I recently followed the build a to do list app with SwiftUI and Firestore run by Peter Friese on YouTube. The ToDos had a boolean completed attribute to indicate whether or not they are complete and they are fetched with a snapshot listener.
Below is an example of a ToDo item that is stored and retrieved from my Firestore collection.
struct ToDo: Identifiable, Codable {
#DocumentID var id: String?
var title: String = ""
var completionDate: Date?
var userId: String?
}
I wanted to achieve the following functionality:
Incomplete ToDos would be fetched
Complete ToDos in the last 24 hours would be fetched
Due to the time functionality I swapped the Boolean attribute for a completion date that gets updated every time the user checks or unchecks whether or not they have complete a ToDo.
Regarding my last question, I discovered that I cannot query for ToDos that have a nil value nor can I make this an OR statement to retrieve the completed ToDos in the last 24 hours in one query.
Is it possible to have the above functionality with one query and one snapshot listener?
Edit 1
Below is an extract from my data model class that fetches my tasks from my firestore collection. The comments are what I want the query below to reflect.
import Foundation
import Firebase
class TaskData: ObservableObject {
#Published var tasks: [Task] = []
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
init() {
subscribe()
}
deinit {
unsubscribe()
}
func subscribe() {
guard let userId = Auth.auth().currentUser?.uid else { return }
if listenerRegistration != nil {
unsubscribe()
}
/*
complete = true
completedDate = Date.now - 24 hours
OR
completed = false
*/
let query = db.collection("tasks")
.whereField("userId", isEqualTo: userId)
listenerRegistration = query.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("No documents in 'tasks' collection")
return
}
self.tasks = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Task.self)
}
}
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
}
If you default the completionDate to a timestamp very far in the future, you can get all documents that were completed in the past 24h and that are not yet completed with a single query on that field.
todoRef.whereField("completionDate", isGreaterThan: timestampOfYesterday)
In your application code, you will then have to identify the documents with the default completionDate and mark them as incomplete in your UI.

How do I retrieve CloudKit Reference List?

I have setup my CloudKit record types the same as my local structs and models in my code but can't seem to find a way to retrieve the list of references.
For example, I have a UserData record type with a record field of friends which is a list of references to Friend record type.
Local models:
struct User {
let id: UUID
let friends: [Friend]
let username: String
}
struct Friend {
let id: UUID
let username: String
}
And this is the code I use to attempt to retrieve the friends
#State var friends = [Friend]()
if let userID = UserDefaults.standard.string(forKey: "userID") {
let publicDatabase = CKContainer.default().publicCloudDatabase
publicDatabase.fetch(withRecordID: CKRecord.ID(recordName: userID)) { (record, error) in
if let fetchedInfo = record {
friends = (fetchedInfo["friends"] as? [Friend])!
}
}
}
So I guess the main question is how do I retrieve a list record field from CloudKit as it does not seem to return the value as an array.

Add document to Firestore sub-collection dynamically - SwiftUI

new to SwiftUI and Firebase. I'm trying to add a document to a Firestore sub-collection. I have the following code in my view model that works great, if I manually add the document ID to the path.
func addOrgData(newOrgs: Org) {
do {
let _ = try db.collection("Company").document("9gwENeCMefPEJPOLvuXg").collection("Employees").addDocument(from: newOrgs)
}
catch {
print(error)
}
}
What I can't figure out is how to grab that document ID and pass it to that function. The basic idea would be to get a list of companies, tap on one, be taken to a list of employees, have an option to add an employee to that list of employees for that company. Any help would be appreciated. Happy to add any other parts of my code, if needed.
To add more clarity to what I'm trying to do, the idea would be to present the user with a list of companies, user taps on a company (Acme) and is presented with a list of existing employees and there is a button on the nav bar that allows the user to add a new employee to that company.
I updated my code to:
let newEmpRef = db.collection("Company").document()
try newEmpRef.collection("Employees").setData(from: newEmployee)
This works in that it creates a new user at that level of the DB, but it creates a new company document with no data and a subcollection of that new empty document. I can't figure out how, if the user is at the list of Acme employees and taps the "Add Employee" button, that it adds the new employee to the already existing Acme > Employees subcollection. DB structure looks like:
Company (collection)
-> Acme (document)
-> Employees (Collection)
-> Marge Simpson (document)
How do I get the document ID for Acme and pass it to the VM to ensure when I create Homer Simpson, he ends up in Acme -> Employees?
Org View Model to get list of Organizations:
class OrgViewModel: ObservableObject {
#Published var org = [Org]()
private var db = Firestore.firestore()
func fetchOrgData() {
db.collection("Organizations").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents")
return
}
self.org = documents.compactMap { (queryDocumentSnapshot) -> Org? in
return try? queryDocumentSnapshot.data(as: Org.self)
}
}
}
}
Employee view model to add new employee:
class EmployeeViewModel: ObservableObject {
#Published var newEmployees: Employee
init(newEmployees: Employee = Employee(id: "", firstName: "", lastName: "", orgName: "")) {
self.newEmployees = newEmployees
}
private var db = Firestore.firestore()
func addEmployeeData(newEmployees: Employee) {
do {
let orgRef = db.collection("Organizations").document()
try orgRef.collection("Employees").document().setData(from: newEmployees)
}
catch {
print(error)
}
}
func save() {
addEmployeeData(newEmployees: newEmployees)
}
}
In this line: let newEmpRef = db.collection("Company").document() you are creating a new document. That's why it adds the user to a new document and sub collection rather than "Acme"
Somewhere in your Company view model, there should be a firebase document with an ID. Pass that ID to your document call in the above function:
db.collection("Company").document("ID FROM VIEW MODEL")
https://firebase.google.com/docs/firestore/query-data/get-data#get_a_document
This way, your changes will be made to the right document and sub collection (employees).
edit: to have access to the right ID, you need to get it from a firebase document reference.
I would do this by passing it to the EmployeeViewModel initializer, and adding a member to the class:
private var orgId: String
init(newEmployees: Employee = Employee(id: "", firstName: "", lastName: "", orgName: ""), orgId: String) {
self.newEmployees = newEmployees
self.orgId = orgId
}
then use it:
func addEmployeeData(newEmployees: Employee) {
do {
// here:
let orgRef = db.collection("Organizations").document(self.orgId)
try orgRef.collection("Employees").document().setData(from: newEmployees)
}
catch {
print(error)
}
}

Swift+Firestore - return from getDocument() function

I'm currently dealing with this problem. I have Firestore database. My goal is to fill friends array with User entities after being fetched. I call fetchFriends, which fetches currently logged user, that has friends array in it (each item is ID of friend). friends array is then looped and each ID of friend is fetched and new entity User is made. I want to map this friends array to friends Published variable. What I did there does not work and I'm not able to come up with some solution.
Firestore DB
User
- name: String
- friends: [String]
User model
struct User: Identifiable, Codable {
#DocumentID var id: String?
var name: String?
var email: String
var photoURL: URL?
var friends: [String]?
}
User ViewModel
#Published var friends = [User?]()
func fetchFriends(uid: String) {
let userRef = db.collection("users").document(uid)
userRef.addSnapshotListener { documentSnapshot, error in
do {
guard let user = try documentSnapshot?.data(as: User.self) else {
return
}
self.friends = user.friends!.compactMap({ friendUid in
self.fetchUserAndReturn(uid: friendUid) { friend in
return friend
}
})
}
catch {
print(error)
}
}
}
func fetchUserAndReturn(uid: String, callback:#escaping (User)->User) {
let friendRef = db.collection("users").document(uid)
friendRef.getDocument { document, error in
callback(try! document?.data(as: User.self) as! User)
}
}
One option is to use DispatchGroups to group up the reading of the users but really, the code in the question is not that far off.
There really is no need for compactMap as user id's are unique and so are documentId's within the same collection so there shouldn't be an issue where there are duplicate userUid's as friends.
Using the user object in the question, here's how to populate the friends array
func fetchFriends(uid: String) {
let userRef = db.collection("users").document(uid)
userRef.addSnapshotListener { documentSnapshot, error in
guard let user = try! documentSnapshot?.data(as: User.self) else { return }
user.friends!.forEach( { friendUid in
self.fetchUserAndReturn(uid: friendUid, completion: { returnedFriend in
self.friendsArray.append(returnedFriend)
})
})
}
}
func fetchUserAndReturn(uid: String, completion: #escaping ( MyUser ) -> Void ) {
let userDocument = self.db.collection("users").document(uid)
userDocument.getDocument(completion: { document, error in
guard let user = try! document?.data(as: User.self) else { return }
completion(user)
})
}
Note that I removed all the error checking for brevity so be sure to include checking for Firebase errors as well as nil objects.

How do I overwrite an identifiable object by refrencing it with an ID in swiftui?

I've simplified my code down so it's easier to see what i'm trying to do, I am building a feed of ratings that are sourced from firebase documents of whomever I am "following"
All that I want to do is whenever firebase says there is a new update to one of my reviews, change that Identifiable, as apposed to having to re-load every review from every person i'm following.
Here's my view:
import SwiftUI
import Firebase
struct Rating: Identifiable {
var id: String
var review: String
var likes: Int
}
struct Home: View {
#ObservedObject var feedViewModel = FeedViewModel()
var body: some View {
VStack{
ForEach(self.feedViewModel.allRatings, id: \.id){ rating in
Text(rating.review)
Text("\(rating.likes)")
}
}
}
}
Here are my functions for FeedViewModel:
class FeedViewModel: ObservableObject {
#Published var following = ["qedXpEcaRLhIa6zWjfJC", "1nDyDT4bIa7LBEaYGjHG", "4c9ZSPTQm2zZqztNlVUp", "YlvnziMdW8VfibEyCUws"]
#Published var allRatings = [Rating]()
init() {
for doc in following {
// For each person (aka doc) I am following we need to load all of it's ratings and connect the listeners
getRatings(doc: doc)
initializeListener(doc: doc)
}
}
func getRatings(doc: String) {
db.collection("ratings").document(doc).collection("public").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
// We have to use += because we are loading lots of different documents into the allRatings array, allRatings turns into our feed of reviews.
self.allRatings += querySnapshot!.documents.map { queryDocumentSnapshot -> Rating in
let data = queryDocumentSnapshot.data()
let id = ("\(doc)-\(queryDocumentSnapshot.documentID)")
let review = data["review"] as? String ?? ""
let likes = data["likes"] as? Int ?? 0
return Rating(id: id, review: review, likes: likes)
}
}
}
func initializeListener(doc: String){
db.collection("ratings").document(doc).collection("public")
.addSnapshotListener { (querySnapshot, error) in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates")
return
}
snapshot.documentChanges.forEach { change in
for document in snapshot.documents{
let data = document.data()
let id = ("\(doc)-\(document.documentID)")
let review = data["review"] as? String ?? ""
let likes = data["likes"] as? Int ?? 0
// I thought swiftui Identifiables would be smart enough to not add another identifiable if there is already an existing one with the same id, but I was wrong, this just duplicates the rating
// self.allRatings.append(Rating(id: id, review: review, likes: likes))
// This is my current solution, to overwrite the Rating in the allRatings array, however I can not figure out how to get the index of the changed rating
// "0" should be replaced with whatever Rating index is being changed
self.allRatings[0] = Rating(id: id, review: review, likes: likes)
}
}
}
}
}
All I want is to make the "likes" of each rating live and update whenever someone likes a rating. It seems simple but I am relatively new to swiftui so I might be completely off on how I am doing this. Any help is greatly appreciated!
After a week of trying to figure this out I finally got it, hopefully this helps somebody, if you want to overwrite an identifiable in your array find the index by using your id. In my case my ID is the firebaseCollectionID + firebaseDocumentID, so this makes it always 100% unique AND it allows me to reference it from a snapshot listener...
let index = self.allRatings.firstIndex { (Rating) -> Bool in
Rating.id == id
}
Then overwrite it by doing:
self.allRatings[index!] = Rating(id: id, review: review, likes: likes)