Swift+Firestore - return from getDocument() function - swift

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.

Related

How to make ForEach loop for a Codable Swift Struct's Dictionary (based on Firestore map)

I am trying to do a ForEach loop that lists all of the social medias a user might have. This would be on a scrollable list, the equivalent of music streaming apps have a list of all the songs you save in your library. The user's social medias list is in reality a dictionary.
My MainViewModel class implements all of the Firebase functionality. See below.
class MainViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var user: User?
init() {
DispatchQueue.main.async {
self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
}
readCodableUserWithMap()
}
#Published var isUserCurrentlyLoggedOut = false
func handleSignOut() {
isUserCurrentlyLoggedOut.toggle()
try? FirebaseManager.shared.auth.signOut()
}
func readCodableUserWithMap() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
print("FAILED TO FIND UID")
return
}
let userID = uid
let docRef = FirebaseManager.shared.firestore.collection("users").document(userID)
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: User.self)
if let mappedField = user.socials {
mappedField.forEach { print($0.key, $0.value) }
}
}
}
}
}
readCodableUserWithMap() is supposed to initialize my codable struct, which represents a user. See below.
struct User: Identifiable, Codable {
#DocumentID var id: String?
var socials: [String: String]?
var uid, email, name, bio, profileImageUrl: String?
var numSocials, followers, following: Int?
}
QUESTION AT HAND: In my Dashboard View, I am trying to have a list of all the social medias a user can have. I can't manage to make a ForEach loop for my user.
I do:
ForEach(vm.user?.socials.sorted(by: >), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
This gives me the following errors:
Value of optional type '[Dictionary<String, String>.Element]?' (aka 'Optional<Array<(key: String, value: String)>>') must be unwrapped to a value of type '[Dictionary<String, String>.Element]' (aka 'Array<(key: String, value: String)>')
Value of optional type '[String : String]?' must be unwrapped to refer to member 'sorted' of wrapped base type '[String : String]'
I have tried coalescing using ?? but that doesn't work, although I assume I am doing something wrong. Force-unwrapping is something I tried but it made the app crash when it was an empty dictionary.
Just in case, here is the database hierarchy: firebase hierarchy
TLDR: HOW can I make a ForEach loop to list all of my user's medias?
You have both vm.user and socials as optionals.
The ForEach loop requires non-optionals, so
you could try the following approach to unwrap those for the ForEach loop.
if let user = vm.user, let socials = user.socials {
ForEach(socials.sorted(by: > ), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
}

SwiftUI: compiler takes initialized value from ViewModel rather than the value that has been fetched

I'm writing a program where I reference a database where authenticated users each have a document whose ID corresponds to their User ID. Given the user's ID, I am trying to determine their name; I have managed to read all of the user's data and it is in my data model of class Users:
class Users {
var id: String
var name: String
var surname: String // ...
}
In my ViewModel, I have
#Published var specificUser = User(id: "", name: "", surname: "", email: "", profficiency: 0, lists: [[]])
which is an initialized user.
In that same ViewModel, I have a function that fetches the User Data from the database, which appears to work. It should then store the new user data in the specificUserData variable.
func getData() {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
self.specificUser = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
print("Error while fetching user's specific data")
}
}
}
Here's how I initialized the getData() function:
init() {
model.getData()
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
I am trying to reference my ViewModel like this:
#ObservedObject var model = ViewModel()
Now here's the problem: when I try to reference the User's name from the view model in my struct with
model.specificUser.name
It gives me the default name, even though I have initialized the getData() function already. Checking my compiler log and adding a bunch of print statements, it appears that the initialization is in fact working, but it is printing data retrieval complete before it is printing the albeit correct name.
Any thoughts? It seems that the initializer function is taking the initialized value from my ViewModel rather than the correct value it should be computing.
try this
func getData(_ completion: #escaping (Bool, User?) -> ()) {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
let user = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
completion(true, user)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
completion(false, nil)
print("Error while fetching user's specific data")
}
}
}
init() {
model.getData() { res, user in
if res {
self.specificUser = user!
}
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
}

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)

Firebase Firestore fetching data with an ID reference then fetching the reference

After searching for a few hours for the answer to this question, I have found 1 post that was similar here: However I tried to replicate but I believe the difference in language syntax made it very hard to translate.
Within my application, users are allowed to make posts, the structure for the post in Firsestore looks like this:
The creator is a userId of a user that also lives in the database.
I am aware of how to fetch things from Firestore when my structs conform to Codable and they map 1 to 1 but I have not experienced having to fetch nested data after an initial fetch.
Question
By querying my backend for posts, how can I also create the user object that lives inside?
Here is the post object I was expecting to create:
import FirebaseFirestoreSwift
public struct Post: Codable {
/// The school id
#DocumentID var id: String?
/// The name of the school
public let content: String
/// The user who made the post
public var creator: AppUser?
}
I want to create appUser from the creator field that is returned. Should I build the content and then have some sort of promise.then to fetch the user? Or can i do both at the same time?
Here is what I think I should be doing
public func fetch(for schoolId: String) -> Promise<[Post]> {
return Promise { resolver in
fireStore
.collection("schools").document(schoolId)
.collection("posts").getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {
resolver.reject(Errors.firebaseError)
return
}
let posts = documents.compactMap { queryDocumentSnapshot -> Post? in
return try? queryDocumentSnapshot.data(as: Post.self)
}
let postsWithUser: [Post] = posts.map { post in
//Fetch User and return an updated struct
}
resolver.fulfill(postsWithUser)
}
}
}
I solved it! Basically, we want to let the first fetch complete. Then we iterate through each post.id and call FetchUser() i which is a function i built that returns Promise<User>
func fetchTopLevelPost(for schoolId: String) -> Promise<[Post]> {
return Promise { resolver in
fireStore
.collection("schools").document(schoolId)
.collection("posts").getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {
resolver.reject(Errors.firebaseError)
return
}
let posts = documents.compactMap { queryDocumentSnapshot -> Post? in
return try? queryDocumentSnapshot.data(as: Post.self)
}
resolver.fulfill(posts)
}
}
}
func fetchPostUser(for posts: [Post]) -> Promise<[Post]> {
let allPromise = posts.map{ FetchUser().fetch(for: $0.creatorId) }
return Promise { resolver in
when(fulfilled: allPromise).done { users in
let completePost = zip(users, posts).map(post.init(completeData:))
resolver.fulfill(completePost)
}
.catch { error in
resolver.reject(Errors.firebaseError)
}
}
}
Here is the callsite:
public func fetch(for schoolId: String) -> Promise<[Post]> {
return fetchTopLevelPost(for: schoolId)
.then { self.fetchPostUser(for: $0) }
}

Make friends based on BOOL status, finding difficult comparing the array value , Firebase, Swift

I am making a chat app,I want to make friends only under the condition that, if a userA has friends userB == ture and the same userB has userA == true , I want them to make friend. To make you more clear here is the json format:
user_friends: {
$userA: {
friends: {
$userB: true
}
},
$userB: {
friends: {
$userA: true,
$userC: true
}
},
$userC: : {
friends: {
$userA: true,
$userB: true
}
}
}
As the json above , userA will have userB as friend, userB will have userA & userC as friend and userC will have only userB as friend because userA do not have userC as his friend. Here is my data model, not sure if it is perfect
import Foundation
import SwiftyJSON
struct Friends {
var myFriends:[Bool]!
struct Friend {
let key: String
let is_friend: Bool
init(snap: FIRDataSnapshot) {
self.key = snap.key
self.is_friend = (snap.value != nil)
}
}
And here is the function by which I am trying to fetch the data and compare them, but its not working as I want
func getFriendList() {
if self.user != nil {
_ = self.ref.child("user_friends").child((user?.uid)!).observeSingleEvent(of: .value, with: { snapshot in
for snap in snapshot.children {
for friends in (snap as AnyObject).children {
// cast friend and append to myFriends
print(friends)
let friend = Friend(snap: friends as! FIRDataSnapshot)
self.myFriends.append(friend)
print("my friends are\(self.myFriends.description)")
print("friends count\(self.myFriends.count)")
if (self.myFriends.count > 0) {
for friend in self.myFriends {
_ = self.ref
.child("user_friends")
.child(friend.key)
.child("friends")
//.child((self.user?.uid)!)
.observeSingleEvent(of: .value, with: { snapshot in
print("Desired Snapshot\(snapshot)")
//Now I need to check the condition here
})
}
}
}
}
})
}//FIRAuth ends here
}//getFriendList ends here
And here is my actual database design: and the debug result I guess I need to take two dictionary and compare their values. Its almost like instagram feature that if someone follows a user and the user follow him back they got message option enabled , message option does not open until they follow each other. Please any kind person come forward and help me. I am in real trouble. Thanks.
You are doing alot of looping and parsing that you dont need to. This is how I would do what you are trying to do:
// A struct for friend nodes on a user account
struct Friend {
let key: String
let is_friend: Bool
init(snap: FIRDataSnapshot) {
self.key = snap.key
self.is_friend = snap.value as! Bool
}
}
// A struct for user account nodes
struct ChatUser {
var friends = [Friend]()
let user_ref: FIRDatabaseReference
let key: String
init(user_ref: FIRDatabaseReference) {
self.user_ref = user_ref
self.key = user_ref.key
}
}
let currentUser = ChatUser(user_ref: self.user.ref)
func getFriendListForUser(user: ChatUser) {
_ = user.ref.child("user_friends").observeSingleEvent(of: .value, with: { snapshot in
for friend in snapshot.children {
// cast friend and append to myFriends
let friend = Friend(snap: friend)
user.friends.append(friend)
}
})
}
// validate friend list in firebase for a given chat user
func validatedFriendsForUser(user: ChatUser) {
for friend in user.friends {
_ = FIRDatabase.database()
.reference(withPath: friend.key)
.child("friends")
.child(self.currentUser.key)
.observeSingleEvent(of: .value, with: { snapshot in
if let status = snapshot as? Bool {
friend.is_friend = status
}
})
}
// Remove friends which are not confirmed in firebase
self.currentUser.friends = user.friends.filter{ $0 }
}
// call our methods to fetch and filter user friends for current user
self.getFriendListForUser(user: self.currentUser)
self.validatedFriendsForUser(user: self.currentUser)
The validatedFriendsForUser() is not required, but if you want to check to see if user A has user B's key set to true in the database.