Firebase + Concurrency

Is concurrency the most difficult concept in Swift? I am having an issue loading data from Cloud Firestore, and I’m pretty sure it’s related to concurrency.

Chris makes it look so easy in the Learning App. As far as I can see, my code is pretty similar, except his works and mine doesn’t…

My model:

struct User: Identifiable {
    var id = ""
    var nickname = ""
    var iLike = [""]
    var likeMe = [""]
}

My content model:

import Foundation
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift

class ContentModel: ObservableObject {
    
    // reference to Cloud Firestore database
    let db = Firestore.firestore()
    
    // Likes
    @Published var usersWhoLikeMe = [User]()
    @Published var usersWhoILike = [User]()
    @Published var myFriends = [User]()
    
    
    
    func getUsersWhoLikeMe() {
        
        let ref = db.collection("users").whereField("iLike", arrayContains: Auth.auth().currentUser!.uid)
        
        ref.getDocuments { snapshot, error in
            if error == nil {

                var userList = [User]()
                
                for doc in snapshot!.documents {
                    
                    let m = User(
                        nickname: doc["nickname"] as? String ?? "",
                        iLike: doc["iLike"] as? [String] ?? [""],
                        likeMe: doc["likeMe"] as? [String] ?? [""]
                    )
                    
                    userList.append(m)
                }
                
                DispatchQueue.main.async {
                    self.usersWhoLikeMe = userList
                }
            }
        }
    }
}

Any suggestions?

1 Like

@CeilingTiles

Hi Brent,

What does your data look like in Firestore?

Realistically the only field you need on each user is who they like, aka iLike. You don’t need a likeMe property since you can interrogate a list of users who Like You where they have your userId in their iLike array.

Your own iLike list details who you like.

Does that make sense?

It makes total sense.

The reason I separated iLike and likeMe into two distinct arrays is for efficiency. As it stands, I can create an array of User from a single Firebase Query.

If I eliminate the likeMe array and instead only look at iLike, would it not simply return an array of user id’s? If I wanted to know more about each of those users—like their name, for example—wouldn’t I then have to loop through all of those id’s and query each user’s info individually? If I had 100 users, that would be 100 queries, versus the one query I am currently doing. I may be wrong—I often am—which is why I’m here. But that’s my thinking… (I’d love to hear your thoughts on this.)

Regardless—my question is actually about concurrency. I am confident my data is in fact being read from Firebase, but it is not being assigned correctly to the self.usersWhoLikeMe variable … and I can’t figure out why.

Thoughts?

It will return a number of documents in which each contains a nickname and an array of userId’s in the iLike array in which there is an entry that matches your whereField clause.

At the moment your User struct is:

struct User: Identifiable {
    var id = ""
    var nickname = ""
    var iLike = [""]
    var likeMe = [""]
}

but it could easily be this:

struct User: Identifiable {
    var id = ""
    var nickname = ""
    var fullname = "" //  Users full name
    var iLike = [""]
    var likeMe = [""]
}

What does your test data look like in Firebase that you are using to prove that the whereField clause finds a match in iLike?

For the purposes of testing this is what I set up on Firebase (using your data model with the exception that id = UUID() so that the User struct conforms to Identifiable without having to make sure that id is some random unique string)
NOTE that the strings in iLike and likeMe are names but they could easily be uid’s. The important point is you want to know who likes you:

Screen Shot 2022-12-04 at 07.44.53

Screen Shot 2022-12-04 at 07.45.09

Let’s assume that you are Fred Nurk.

let ref = db.collection("users1").whereField("iLike", arrayContains: "Fred Nurk")
which retrieved two records since “Fred Nurk” likes mrNosey and funnyguy.

Let’s assume that you are Barney Rubble.

let ref = db.collection("users1").whereField("iLike", arrayContains: "Barney Rubble")
which retrieved one record since “Barney Rubble” likes funnyguy.

So the code you have works as far as I can tell.

Thanks again for your feedback. I really (really) appreciate it.

I will make the id property a UUID. That seems obvious now that you said it. (I suppose I didn’t because the UUIDs are generated automatically by Firebase, so I figured I could store that in a string.)

To be clear: my User struct contains a bunch more properties than I listed—things like nickname, age, gender, etc.—I just didn’t include them in the question as I didn’t think they’d have any bearing on the answer.

I know the code looks like it should work—it looks a lot like Chris’s in the Learning App, and his works—but mine doesn’t.

In the meantime, I hard coded some new instances of User in so I could move on and work on other things. What’s interesting though, is after appending those hardcoded users to my userWhoLikeMe array, I get one or two that I didn’t hard code and were actually loaded from the database. The problem is it’s not all of them, which is how I know I have a concurrency problem.

If your intention was to store the currentUser.uid as the document id for the Firebase user record then that would be fine to declare it as a String since when you recall the matching document from Firebase you can store the doc.documentID as the id in your new User record that you are appending to usersWhoLikeMe.

I’m not so sure that you are having a concurrency issue since if you retrieve a matching record then it’s working. The issue is more likely data related that does not match.

That makes sense. Thanks!