0

I'm using swift's DispatchGroup() to help orchestrate a for loop that

  • finds a document in firesbase
  • converts the document to a custom object
  • appends the custom object to an array

With each pass, the function ends up appending each object twice to the array and I can't understand why. Here is the function...

    func getFriends() {
    
    // Initialize the DispatchGroup
    let group = DispatchGroup()

    // the myFriends array contains documentIDs that I am using to fetch documents from firebase
    //
    for pid in myFriendObj.myFriends {
        group.enter()
        
        _ = Firestore.firestore().collection("Players")
            .whereField(FieldPath.documentID(), isEqualTo:  pid)
            .addSnapshotListener { [self] querySnapshot, error in

                if let error = error {
                    print("Error getting > Players: \(error.localizedDescription)")
                    return
                }
                guard let querySnapshot = querySnapshot else { return }

                self.players.append(
                    contentsOf: querySnapshot.documents.compactMap { document in
                    try? document.data(as: UserProfile.self)
                })
           
                group.leave()
                
         }
    }
    
    group.notify(queue: DispatchQueue.global(qos: .background)) {
        // I'm currently eliminating the dups via this fancy extends method.
        self.players = self.players.removeDuplicates()
    }
    
}

:: UPDATE ::

Still no luck on this - i've even removed dispatchgroup and the snapshotlistener callbacks and still this code calls get() twice when an instance of the class is instantiated. Here is the new, more simple code...

class FriendRepository: ObservableObject {

private    let store = Firestore.firestore()
private    let friendPath: String = "MyFriends"
@Published var friendIDs: [String] = []

var userId = ""

private let authenticationService = AuthenticationService()
private var cancellables: Set<AnyCancellable> = []


init() {
    authenticationService.$user
        .compactMap { user in
            user?.uid
        }
        .assign(to: \.userId, on: self)
        .store(in: &cancellables)

    authenticationService.$user
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.get()
        }
        .store(in: &cancellables)
}


 

func get( ) {
    store.collection(friendPath).document(userId).getDocument {(document, error) in
        let result = Result {
          try document?.data(as: Friends.self)
        }
        switch result {
        case .success(let f):
            if let f = f {
                print("friends:>> \(f.myFriends)")
                self.friendIDs = f.myFriends
            } else {
                print("Document does not exist")
            }
        case .failure(let error):
            print("Error decoding city: \(error)")
        }
    }
}

When a new instance run init(), I see this in the console... It prints the friends:>> statement twice

friends:>> ["PHyUe6mAc3LodM5guJJU"]
friends:>> ["PHyUe6mAc3LodM5guJJU"]
phil
  • 993
  • 1
  • 10
  • 34

2 Answers2

1

Each time a change happens in the database, your addSnapshotListener closure gets called with all data that matches the query - even if that data wasn't change since the last call. This typically means that you'll want to empty self.players at the top of the callback, or loop over the documentChanges collection to determine exactly what changed.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you, Frank! So I've been tinkering with clearing out self.players but am not sure where to do the removeAll() statement. You mention "at the top of the callback" but I'm not sure that I'm using a callback. Can you indicate where to place this line? – phil Mar 18 '21 at 04:09
  • 1
    A closure/callback is a block of code that you pass into a function call, that then gets called at some moment by that function. Here: the code block inside `.addSnapshotListener { [self] querySnapshot, error in` is a callback/closure, you can clear the list right above `if let error = error {`. – Frank van Puffelen Mar 18 '21 at 14:36
  • Frank, no luck man. I've greatly simplified things and removed the callback as well as the dispatch group and I'm even do a single firestore document read and still it does two reads when a class instance starts. Check out the new code where it says :: UPDATED :: – phil Mar 19 '21 at 01:16
  • 1
    Hmm... in that case I'm not sure what could be the problem. I hope someone with better Swift/SwiftUI skills sees what's going on. – Frank van Puffelen Mar 19 '21 at 01:54
0
    func getFriends() {
// this will empty the players array when ever the get friends function gets called. 
self.players.removeAll()
// Initialize the DispatchGroup
let group = DispatchGroup()

// the myFriends array contains documentIDs that I am using to fetch documents from firebase
//
for pid in myFriendObj.myFriends {
    group.enter()
    
    _ = Firestore.firestore().collection("Players")
        .whereField(FieldPath.documentID(), isEqualTo:  pid)
        .addSnapshotListener { [self] querySnapshot, error in

            if let error = error {
                print("Error getting > Players: \(error.localizedDescription)")
                return
            }
            guard let querySnapshot = querySnapshot else { return }

            self.players.append(
                contentsOf: querySnapshot.documents.compactMap { document in
                try? document.data(as: UserProfile.self)
            })
       
            group.leave()
   
     }
}

}