Damiano Miazzi
Damiano Miazzi

Reputation: 2325

Firebase and swiftUI, listening for real time update strange behave weird

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

Upvotes: 0

Views: 1841

Answers (1)

dig
dig

Reputation: 255

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()
                     }
                }
           }

Upvotes: 3

Related Questions