Home > Mobile >  Bypassing Firebase IN: limit in Swift
Bypassing Firebase IN: limit in Swift

Time:12-05

I have a query that I am doing with Google's Firestore where I get a query of posts from a collection. This is a central collection of all posts on the network but I want to be able to filter what comes from the server so the processing/filtering doesn't happen on the client side. Essentially, I want to get a feed of posts from only account the user follows.

Creating that array is easy enough. When the user logins it downloads the array of UID the user follows. This array, theoretically, could be long. Firestore has this handy '.whereField' option that can filter look through a String Array using in: like this.

.whereField("userUID", in: auth.userFollowedAccounts!)

It works perfectly but according to the documentation Firestore only allowed 10 items in the array when using in:. This causes an issues because I want users to be able to follow more then 10 accounts. I saw some other solutions from some other languages to get around this issue by splicing the array or doing some clever looping to go though all the options. Unfortunately, I can't seem to find a solution for Swift. Would love someone to take a look and help me brainstorm some work arounds for this.

// Full Code Block

    func getData() {
    let db = Firestore.firestore()
    db.collection("posts")
        .order(by: "dateCreated", descending: true)
        .whereField("userUID", in: auth.userFollowedAccounts!)
        .limit(to: 10)
        .getDocuments { (snap, err) in
        
        if err != nil {
            print((err?.localizedDescription)!)
            return
        }
        
        self.post.removeAll()
        
        for i in snap!.documents {
            let data = Post(id: i.documentID, username: i.get("username") as? String, fullName: i.get("fullName") as? String, profilePictureURL: i.get("profilePictureURL") as? String, link: i.get("link") as? String, location: i.get("location") as? String, postImage: i.get("postImage") as? String, isVerified: i.get("isVerified") as? Bool, caption: i.get("caption") as? String, likeCounter: i.get("likeCounter") as? Int, dateCreated: i.get("dateCreated") as? String, userUID: i.get("userUID") as? String, isAdmin: i.get("isAdmin") as? Bool, pronouns: i.get("pronouns") as? String)
            
                self.post.append(data)
        }
        
        self.lastDoc = snap!.documents.last
    }
}

Let me know if you have any questions.

CodePudding user response:

Here's a simple example using Firestore async/await calls to shorten the code.

The Firebase structure was not included in the question (please include that in questions) so I will craft one which may be similar to what you're using

Starting with a users collection which uses the users uid as the documentId, keeps the users name and then the users who they are following as an array

users
   uid_0
      user_name: "Leroy"
      following
          0: uid_9
   uid_1
      user_name: "Biff"
      following
          0: uid_0
          1: uid_2

In this, uid_0 is following uid_9 and uid_1 is following uid_0 and uid_2

Then a posts collection snippit

posts
   post_0
      post_msg: "Post 0 from uid_0"
      post_uid: "uid_0"
   post_1
      post_msg: "Post 0 from uid_1"
      post_uid: "uid_1"
   post_2
      post_msg: "Post 1 from uid_0"
      post_uid: "uid_0"

The posts have a message and the uid of the user that posted it.

Then the code.

func getPostsOfUsersIFollow() {
    let usersCollection = self.db.collection("users")
    let postsCollection = self.db.collection("posts")
    let thisUserDoc = usersCollection.document("uid_1") //me
    
    Task {
        do {
            let userResult = try await thisUserDoc.getDocument()
            let usersIFollow = userResult.get("following") as? [String] ?? []
            
            print(usersIFollow) //outputs the list of users uid's I follow
            
            for uid in usersIFollow {
                let usersPosts = try await postsCollection.whereField("post_uid", isEqualTo: uid).getDocuments()
                for postDoc in usersPosts.documents {
                    let postMsg = postDoc.get("post_msg") as? String ?? "no msg"
                    print("post_id: \(postDoc.documentID)   uid: \(uid) posted msg: \(postMsg)")
                }
            }
        } catch {
            print("need to handle error")
        }
    }
}

If I am user uid_1, I am following uid_0 and uid_2. When this code is run, it will first query for all of the users I am following (uid_0, uid_2) then iterate over that list, which can be any number of users, to query the posts from each of those users, and output those posts to console.

So, if uid_0 has 3 posts and uid_2 has 3 posts the final output would look like this

["uid_0", "uid_2"]
post_id: post_0   uid_0 posted msg: Post 0 from uid_0
post_id: post_4   uid_0 posted msg: Post 1 from uid_0
post_id: post_6   uid_0 posted msg: Post 2 from uid_0
post_id: post_2   uid_2 posted msg: Post 0 from uid_2
post_id: post_5   uid_2 posted msg: Post 1 from uid_2
post_id: post_9   uid_2 posted msg: Post 2 from uid_2

In this case I output to console but in code, you'd likely have some class to store the uid, name and post and then populate an array which backs a tableView or collection with that data

class UserPostClass {
   var uid = ""
   var postId = ""
   var userName = ""
   var postMsg = ""
}

var userPostArray = [UserPostClass]()

and then once the array was populated, reload your tableView, view etc.

The one gotcha here is ensuring the UI is responsive - with small datasets this will work as is, but if you're loading thousands of posts (don't do that) you'll likely want to paginate your data to break it into smaller chunks.

The other thing to note is there is no ordering, so you'll likely want to add an orderBy clause

  • Related