0

On my project I'm using firebase Cloud Firestore in order to implement a similar system like "Facebook" to request and confirm friends.

On my app some user are Administrator and some just normal user.

Only normal user can search and add Administrator in order to join some specific event scheduled from the administrator.

here below how is my data in Cloud Firestore :

Admin data

every user have collection with pending and confirm friends.

I' m using a view to list all the pending and confirm friends for each user

Friends View Admin

using the .addSnapshotListener {} of firebase I'm checking with the following function every time there is a change on the confirm and pending friend and I publish the change in 2 @Published arrays, pendingFriendsADMIN = [UserMOdel] and confirmedFriendADMIN = [UserMOdel]

func userUpdateFriendUser(userInfo: UserModel){ db.collection("userUser").document(userInfo.email).collection("pendingFriends") .addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in self.pendingFriendsUSER = [] guard let documents = documentSnapshot?.documents else { print("Error fetching documents: \(String(describing: error?.localizedDescription))") return } var i = 0 for doc in documents { debugPrint("inizio il ciclo pending user\(i)") let idUser = doc["userID"] as? String ?? "no ID" self.downloadImageForAdmin(userID: idUser) { (urlImage) in let userPending = UserModel(name: "", surname: "" ,username: "", email: "", userID: "", adminLevel: "", immagine: urlImage!, position: "centro", position2: "sx", vote: 0) userPending.name = doc["name"] as? String ?? "NA name" userPending.surname = doc["surname"] as? String ?? "NA surname" userPending.adminLevel = doc["adminLevel"] as? String ?? "NA admin" userPending.email = doc["email"] as? String ?? "NA email" userPending.username = doc["username"] as? String ?? "NA username" userPending.userID = doc["userID"] as? String ?? "NA id" userPending.position = doc["position"] as? String ?? "na position" userPending.position2 = doc["position2"] as? String ?? "na position" userPending.vote = doc["vote"] as? Int ?? 0 self.pendingFriendsUSER.append(userPending) i = i+1 debugPrint("finito ciclo pending") } } } db.collection("userUser").document(userInfo.email).collection("confirmedFriend") .addSnapshotListener (includeMetadataChanges: false){ documentSnapshot, error in self.confirmedFriendUSER = [] guard let documents = documentSnapshot?.documents else { print("Error fetching documents: \(String(describing: error?.localizedDescription))") return } for doc in documents { debugPrint("inizio il ciclo confirm user \(i)") let idUser = doc["userID"] as? String ?? "no ID" self.downloadImageForAdmin(userID: idUser) { (urlImage) in let userConfirm = UserModel(name: "", surname: "" ,username: "", email: "", userID: "", adminLevel: "", immagine: urlImage!, position: "centro", position2: "sx", vote: 0) userConfirm.name = doc["name"] as? String ?? "NA name" userConfirm.surname = doc["surname"] as? String ?? "NA surname" userConfirm.adminLevel = doc["adminLevel"] as? String ?? "NA admin" userConfirm.email = doc["email"] as? String ?? "NA email" userConfirm.username = doc["username"] as? String ?? "NA username" userConfirm.userID = doc["userID"] as? String ?? "NA id" userConfirm.position = doc["position"] as? String ?? "na position" userConfirm.position2 = doc["position2"] as? String ?? "na position" userConfirm.vote = doc["vote"] as? Int ?? 0 self.confirmedFriendUSER.append(userConfirm) } } } } 

the similar method is used also for listed the change on the userFriendList.

a user, can search an administrator via the email, and send to him a friend request(see below)

Friend Request

the user sent the friend request with the following function: simply, I write on the pending friend the of the user the admin email and in the admin pending friend the user email

 func sendFriendRequest(userInfo: UserModel, userToRequest: UserModel, closure: @escaping warning){ // check if reuqest already sent self.db.collection("userAdmin").document(userToRequest.email).collection("confirmedFriend").whereField("email", isEqualTo: userInfo.email).getDocuments() { (queryResult, err) in if let err = err { debugPrint("unable to get data , friend alrady request\(err)") } else { if queryResult!.documents.count > 0 { debugPrint("siete gia amici") // mettere warning let warning = true closure(warning) return } else { // if request never sent, metto user nella lista dell admin pending self.db.collection("userAdmin").document(userToRequest.email).collection("pendingFriends").document(userInfo.email).setData([ "username": userInfo.username, "email" : userInfo.email, "userID" : userInfo.userID, "adminLevel": userInfo.adminLevel, "name":userInfo.name, "surname":userInfo.surname, "position": userInfo.position, "position2": userInfo.position2, "vote": userInfo.vote ], merge: false) { (err) in self.db.collection("userUser").document(userInfo.email).collection("pendingFriends").document(userToRequest.email).setData([ "username": userToRequest.username, "email" : userToRequest.email, "userID" : userToRequest.userID, "adminLevel": userToRequest.adminLevel, "name":userToRequest.name, "surname":userToRequest.surname, "position": userToRequest.position, "position2": userToRequest.position2, "vote": userToRequest.vote ], merge: false) } // metto sulla mia pending request } } } } 

Here the problem... some time , not always once I sent the request to a friend admin the .addSnapshotListener duplicate the change , as you can se from the third picture there is 2 time the same pending friend.

If I exit from the view and I go back the pending friend are correct.

here the code of my AdminFriendRequest : View

import SwiftUI import URLImage struct AdminFriendRequest: View { @Binding var dismissView : Bool @ObservedObject var dm : DataManager @Binding var meInfo: UserModel? var body: some View { VStack{ fakebar Spacer() List{ HStack { Image(systemName: "person.2") Text("Pending friends request:") }.font(.headline) .foregroundColor(.blue) ForEach(dm.pendingFriendsADMIN) { friend in HStack{ if friend.immagine == nil{ Image(systemName: "person") .resizable() .frame(width: 30, height: 30, alignment: .center) .clipShape(Circle()) } else { URLImage(friend.immagine!) { proxy in proxy.image .resizable() .frame(width: 30, height: 30, alignment: .center) .clipShape(Circle()) } } Text(friend.username) Spacer() Image(systemName: "checkmark.circle") } .onTapGesture { if self.meInfo != nil { self.dm.tapToConfirmFriend(me: self.meInfo!, friendToConfirm: friend) { (isFriendConfirm) in debugPrint("is friend confirm \(isFriendConfirm)") } } } } if dm.pendingFriendsADMIN.isEmpty { Text("No friend request yet").font(.caption) } HStack { Image(systemName: "person.3") Text("Friends:") }.font(.headline) .foregroundColor(.blue) ForEach(dm.confirmedFriendADMIN) { friend in HStack{ if friend.immagine == nil{ Image(systemName: "person") .resizable() .frame(width: 30, height: 30, alignment: .center) .clipShape(Circle()) } else { URLImage(friend.immagine!) { proxy in proxy.image .resizable() .frame(width: 30, height: 30, alignment: .center) .clipShape(Circle()) } } Text(friend.username) Spacer() Image(systemName: "checkmark.circle").foregroundColor(.green) Button(action: { self.dm.removeFriend(me: self.meInfo!, friendConfirm: friend) }, label: { Text("remove friend") }) }.padding(.all) } }.padding(.trailing) } .onAppear { self.dm.newListUpdateForAdmin(userInfo: self.meInfo!) } } var fakebar: some View { ZStack { HStack { Spacer() Image(systemName: "chevron.compact.down") .font(.system(size: 60)) .aspectRatio(contentMode: .fit) .foregroundColor(.white) Spacer() } HStack { Spacer() Button(action: { self.dismissView.toggle() }) { Text("Close") .fontWeight(.bold) .foregroundColor(.white) .padding(.horizontal) } } } .frame(height: 44) .background(Color.green.padding(.top, -44)) } } 

I use the onAppear to trigger the list update with the .addSnapshotListener

.onAppear { self.dm.newListUpdateForAdmin(userInfo: self.meInfo!) } 

I can't find out why... is it correct the way how i'm using the .addSnapshotListener ? Or any other idea how to handle the friend request. happy to change my way how to deal with the friend request.

Thanks

1 Answer 1

3

Some suggestions that maybe helps:

1. Implement listener state control: These help to control when a user add, modify or remove a record, helps not to reload all data and allow not to duplicate events, for example in the code below I get all users (event documentChange.add) if a new user is added you don't reload all users array.

Api:

 function getUsersPending(userInfo: UserModel, onSuccess: @escaping([UserModel]) -> Void, onError: @escaping(_ errorMessage: String) -> Void, newPendingUser: @escaping(UserModel) -> Void ) { db.collection("userUser").document(userInfo.email).collection("pendingFriends").addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in self.pendingFriendsUSER = [] guard let snapshot = documentSnapshot else { return } var userPendingArray = [UserModel]() snapshot.documentChanges.forEach { (documentChange) in switch documentChange.type { case: .added : let dict = documentChange.document.data() //Get User from firebase doc pendingUser = .... newPendingUser(pendingUser) //escape New User userPendingArray.appen(pendingUser) print("Pending User Added") case .modified : //implements action (new escaping) print("Pending User Modified") case .removed : print("User pending removed") } } onSuccess(userPendingArray) } 

Users pending ViewModel sample

class UserPendingViewModel() : ObservableObject { @Published var usersPending: [UserModel] = [] @Published var isLoading = false var errorString : String = "" func loadUsersPending() { self.usersPending = [] self.isLoading = true dm. getUsersPending(userInfo: userModel, onSuccess: { (users) in if (self.usersPending.isEmpty) { self.usersPending = users } self.isLoading = false }, onError: { (errorMessage) in print("Error Message \(errorMessage)") }, newPendingUser: { (user) in if (!self.usersPending.isEmpty) { self.usersPending.append(user) } }) } } 

View

struct UserPendingView: View { @ObservedObject var model = UserPendingViewModel() var body: some View { ScrollView { if !model.usersPending.isEmpty { ForEach(model.usersPending, id: \.messageId) { user in //Show your data } } }.onAppear{ self.model.loadUsersPending() } } 

2. Activate / Deactivate listeners. If your app is not showing the pending view users no need to keep alive listeners. Activate the listener onAppear and Deactivate onDisappear.

On previous sample New escaping, var listener declaration and result on Api

function getUsersPending(userInfo: UserModel, onSuccess: @escaping([UserModel]) -> Void, onError: @escaping(_ errorMessage: String) -> Void, newPendingUser: @escaping(UserModel) -> Void, listener: @escaping(_ listenerHandle: ListenerRegistration) -> Void ) { ) { let listenerRegistration = db.collection("userUser").document(userInfo.email).collection("pendingFriends").addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in self.pendingFriendsUSER = [] guard let snapshot = documentSnapshot else { return } var userPendingArray = [UserModel]() snapshot.documentChanges.forEach { (documentChange) in switch documentChange.type { case: .added : let dict = documentChange.document.data() //Get User from firebase doc pendingUser = .... newPendingUser(pendingUser) //escape New User userPendingArray.appen(pendingUser) print("Pending User Added") case .modified : //implements action (new escaping) print("Pending User Modified") case .removed : print("User pending removed") } } onSuccess(userPendingArray) } listener(listenerRegistration) //escaping listener } 

Users pending ViewModel sample, declaration listener and add function listener result (Note: import Firebase)

class UserPendingViewModel() : ObservableObject { @Published var usersPending: [UserModel] = [] @Published var isLoading = false var errorString : String = "" var listener : ListenerRegistration! func loadUsersPending() { self.usersPending = [] self.isLoading = true dm. getUsersPending(userInfo: userModel, onSuccess: { (users) in if (self.usersPending.isEmpty) { self.usersPending = users } self.isLoading = false }, onError: { (errorMessage) in print("Error Message \(errorMessage)") }, newPendingUser: { (user) in if (!self.usersPending.isEmpty) { self.usersPending.append(user) } }) { (listener) in self.listener = listener } } } 

View implement onDisappear to disconnect listener

struct UserPendingView: View { @ObservedObject var model = UserPendingViewModel() var body: some View { ScrollView { if !model.usersPending.isEmpty { ForEach(model.usersPending, id: \.messageId) { user in //Show your data } } }.onAppear{ self.model.loadUsersPending() } .onDisappear { if self.model.listener != nil { self.model.listener.remove() } } } 
Sign up to request clarification or add additional context in comments.

3 Comments

hello, thanks for the help, using ur answer I came up with a solution, but i'm getting some problems with closure onSuccess, if I put it like your example I can't see the update, it work only if I put inside the the .added case, but I still get double or more result sometime (can't find out why), I post at this link the question: stackoverflow.com/questions/63214945/…
I totally can't understand why the onSucces can't work outside the forEach
Hi, I don't understand, onSuccess is outside forEach when you load your view and execute your Api, first is getting all your users pending add (case) when forEach finish its send via onSuccess escaping to your viewModel. Thinking about your problem I was a similar problem because on previous calling View, It helps to me to Put a init function in your ViewModel and debug or print then I saw that my view init several times because a problem on the previous View.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.