Bypassing Firebase IN: limit in Swift - swift

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.

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

Is there any way to map user info with their tweets without too many fetching?

I am now trying to create a simple Twitter client using SwiftUI, and currently at the point where I try to fetch my timeline.
Reading the official Twitter API v2 document, there is a way to fetch timeline, but it only offers tweet ID, text, created_at, and author Id.
so I tried to fetch author info using the author Id in loop, which causes too many fetching data and many of them are redundant because a single user tweet many times.
This way makes my account easily reach the api limit, but I am struggling to find better ways.
I am not an enginner and write codes just for fun, so I am not really familiar with techniques enginners should know... I would appreciate if you give me even a hint.
Here is my code below.
func getStream(client: TwitterAPIClient, id: String) async throws {
let response = await client.v2.timeline.getUserReverseChronological(.init(
id: id,
expansions: TwitterTweetExpansionsV2.all, maxResults: 20,
mediaFields: TwitterMediaFieldsV2.all,
tweetFields: TwitterTweetFieldsV2.all.subtracting([.promotedMetrics, .nonPublicMetrics, .organicMetrics]),
userFields: TwitterUserFieldsV2.all)
)
.responseDecodable(type: TwitterDataResponseV2<
[TwitterTimelineV2],
TwitterTimelineV2.Include,
TwitterTimelineV2.Meta>.self)
if let error = response.error {
print(error)
throw error
} else {
print(response.success!)
}
print(response)
let timeline = try response.map { $0.data }.result.get()
for tweet in timeline {
let response = try await getUserInfo(client: client, id: tweet.authorId).result.get()
print(response)
}
}
Note: TwitterAPIClinet is a client of a third party library.
Instead of fetching user data for each individual tweet, you could first loop the timeline array and store each user once in a dictionary. Then, you can request the user info based on that dictionary.
let timeline = try response.map { $0.data }.result.get()
var usersDictionary = [String : Bool]()
for tweet in timeline {
usersDictionary[tweet.authorId] = true
}
for authorId in usersDictionary.keys {
let response = try await getUserInfo(client: client, id: authorId).result.get()
print(response)
}

Swift + Firebase: Storing, retrieving, and using an array of document references

I'm relatively new to Swift and Firebase, so I'm not very familiar with the intricacies of how both work together. I'm building a chat app that has messages and threads. A user can send a message, struct Message, and if another user wants to directly reply to that message, a thread is created. For each message, I'm storing an array of Firebase document references to the other messages in the thread, threadBefore: [Message].
struct Message: Codable, Identifiable {
var id: String
var content: String
var name: String
var upvotes: Int
var likedByUser: Bool
var dontShow: Bool
var sentAt: Date
var threadsArray: [Message]
}
The following is my code for fetching all the chat messages from Firebase:
dontShow property: if dontShow == true means that the message is inside the thread and shouldn't be displayed like a regular message in the chat. However, the very last message in the thread is displayed and has dontShow = false.
func fetchMessages(docId: String, collectionType: String, isThreadMember: Bool) {
if (user != nil) {
db.collection("chatrooms").document(docId).collection(collectionType).order(by: "sentAt", descending: false).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No messages")
return
}
// let threadsTemp: [Message]()
let classroomId = docId
if !isThreadMember {
if collectionType == "messages" {
self.messages = documents.map { doc -> Message in
let data = doc.data()
let docId1 = doc.documentID
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
let threadBefore = data["threadBefore"] as? [DocumentReference] ?? [DocumentReference]()
// using reference array
if dontShow == false {
if (threadBefore.count > 0) {
// reset the temporary array that holds the threads to be added afterwards
self.threadsTemp = []
for docRef in threadBefore {
docRef.getDocument { (doc2, error) in
if let doc2 = doc2, doc2.exists {
let docId2 = doc2.documentID
self.fetchThreadMessages(classroomId: classroomId, parentMessageId: docId1, docId: docId2)
} else {
print("Document does not exist")
}
}
}
}
}
return Message(id: docId1,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: dontShow,
sentAt: sentAt,
threadsArray: self.threadsTemp)
}
}
Another function: fetchThreadMessages:
// fetch a specified message and then append to the temporary threads array, threadsTemp
func fetchThreadMessages(classroomId: String, parentMessageId: String, docId: String) -> Message {
if (user != nil) {
let docRef = db.collection("chatrooms").document(classroomId).collection("messages").document(docId)
docRef.getDocument { (doc, error) in
if let doc = doc, doc.exists {
if let data = doc.data(){
let docId = doc.documentID
print("docid")
print(docId)
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let likedUsersArr = data["likedUsers"] as? Array ?? [""]
// if message is in thread (but not last message), then don't show as normal message, but in thread
let dontShow = data["dontShow"] as? Bool ?? false
let sentAt = data["sentAt"] as? Date ?? Date()
self.threadsTemp.append(Message(id: docId,
content: content,
name: displayName,
upvotes: likedUsersArr.count,
likedByUser: likedUsersArr.contains(self.user!.uid) ? true : false,
dontShow: true,
sentAt: sentAt,
threadsArray: []))
}
}
}
}
}
I haven't implemented how the sendMessage() function updates the threadBefore array, but I'm currently updating this field directly on Firebase just for testing.
func sendMessage(messageContent: String, docId: String, collectionType: String, isReply: Bool, threadId: String) {
if (user != nil) {
if isReply {
let docRef = db.collection("chatrooms").document(docId).collection(collectionType).document(threadId)
self.threadRef = db.document(docRef.path)
}
db.collection("chatrooms").document(docId).collection(collectionType).addDocument(data: [
"sentAt": Date(),
"displayName": user!.email ?? "",
"content": messageContent,
"likedUsers": [String](),
"sender": user!.uid,
"threadBefore": isReply ? [threadRef] : [DocumentReference](),
"dontShow": false])
}
}
A little bit more on how I'm fetching and retrieving the document references from threadsBefore: For each message in the collection, I loop its threadsArray, which consists of DocumentReferences to other messesages that are in that thread. For each of those document references, I run self.fetchThreadMessages. This retrieves that message and stores a Message() instance in threadsTemp. Then, back in self.fetchMessage, when I'm done filling up the self.threadsTemp with all of the documents retrieved from threadsBefore, I store it in threadsArray property in the Message struct.
Now, look at the return state in self.fetchMessages above, the very last assignment inside Message() is threadsArray: self.threadsTemp. But the problem here is that this is just a reference? And it would change based on the last assignment to self.threadsTemp?
I've tried multiple ways to implement this entire storing and retrieving thing. But all came with several complicated errors. I tried using dictionaries, or storing just the document id's for the thread messages and then look them up in self.Messages (since it has all of the messages stored in it).
What's the best way to implement this? Or fix my errors?
I know my code is probably a mishmash of inefficient and confused coding practices. But I'm trying to learn.
With that being said (meaning, my comment above), I'm going to not as much address your code as I am going to propose what I think is a good way to organize your Firestore database for a chat app. Then, hopefully you will be able to apply it to your own situation.
First things first, we have to remember that Firestore charges based on the number of queries you make - it does not take into account the amount of data that is being fetched in a particular query - so fetching a single word is as expensive as fetching an entire novel. As such, we should structure our database in a way that is financially efficient.
What I would do personally is structure my database with two primary sections, (1) conversation threads and (2) users. Each user contains references to their threads they are a part of, and each thread contains a reference to the users that are in that thread. That way we can easily and efficiently fetch a user's conversations, or obtain the users in a particular conversation.
// COL = Collection
// DOC = Document
Threads (COL)
Thread1 (DOC)
id: String // id of the thread
participants: [String: String] = [
user1: [
"username": "Bob23",
"userID": IJ238KD... // the document id of the user
]
] // Keep this "participants" dictionary in the root of the thread
// document so we have easy access to fetch any user we want, if
// we so desire (like if the user clicks to view the profile of a
// user in the chat.
Messages (COL)
Message1 (DOC)
from: String // id of user that sent message
Message2 (DOC)
...
...
Thread 2 (DOC)
...
...
Users (COL)
User1 (DOC)
threadIDs: [String] // Contains the thread "id"s of all the threads
// the user is a part of. (You probably want to
// use a dictionary instead since array's can be
// difficult to work with in a database, but for
// simplicity, I'm going to use an array here.)
User2 (DOC)
...
...
Now, let's say the user opens the app and we need to fetch their conversation threads. It's as easy as
db.collection("Threads").whereField("id", in: user.threadIDs)
[EDIT] user.threads would be a local array of type String. You should have this data on hand since you would be fetching the current user's User document on app launch, which is why we include this data in that user document.
Basically, this query returns every document in the Threads collection whose "id" field exists in the array of the user's threadIDs (which is an array of type String). You can read more about Firestore queries in their docs here.
In other words, we get every thread document that the user has references to.
The great thing about this is that it takes only one query to return all of the conversations of a user.
Now let's say the user is scrolling through their conversation threads in the app, and they tap one of them to open up the messages. Now, all we do is fetch all the messages in that particular thread, again only requiring one query to do so.
Last but not least, if for some reason we have to get info about a particular user in a conversation, we have all the references we need within that thread document itself to fetch the user data, if needed.

How to make a function with a loop asynchronous in Swift?

I am creating an application for a library. I am trying to fetch all the books the user has checked out from Firebase, but my attempts to make the function asynchronous with a DispatchGroup doesn't seem to be working. I suspect this to be because of the for-in loop found inside of the function.
func fetchHistory() {
if items.count > 0 {
items.removeAll()
}
let myGroup = DispatchGroup()
myGroup.enter()
var itemNames = [String]() // this holds the names of the child values of /users/uid/items/ <-- located in Firebase Database
guard let uid = fAuth.currentUser?.uid else {return}
fData.child("users").child(uid).child("items").observe(.value, with: { snapshot in
// make sure there is at least ONE item in the history
if snapshot.childrenCount > 0 {
let values = snapshot.value as! NSDictionary
for i in values.allKeys {
itemNames.append(i as! String)
}
print(itemNames)
let uid = fAuth.currentUser!.uid // get the UID of the user
for item in itemNames {
fData.child("users").child(uid).child("items").child(item).observe(.value, with: { snapshot in
let values = snapshot.value as! NSDictionary
let bookTitle = values["title"] as! String
print(bookTitle)
let bookAuthor = values["author"] as! String
print(bookAuthor)
let bookCoverUrl = values["coverUrl"] as! String
print(bookCoverUrl)
let bookStatus = values["status"] as! String
print(bookStatus)
let bookDueDate = values["dueDate"] as! String
print(bookDueDate)
let book = Book(name: bookTitle, author: bookAuthor, coverUrl: bookCoverUrl, status: bookStatus, dueDate: bookDueDate)
self.items.append(book)
})
}
self.booksTable.isHidden = false
} else {
self.booksTable.isHidden = true
}
})
myGroup.leave()
myGroup.notify(queue: DispatchQueue.main, execute: {
self.booksTable.reloadData()
print("Reloading table")
})
}
Here is the output from the print() statements:
########0
Reloading table
["78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8"]
Mockingjay (The Hunger Games, #3)
Suzanne Collins
https://images.gr-assets.com/books/1358275419s/7260188.jpg
Checked
Replace
The first two lines of output should be printed AFTER everything else has printed. I really need some help on this, I have been stuck on this for hours. Thanks!
Edit:
As requested, here is my Firebase structure:
users:
meZGWn5vhzXpk5Gsh92NhSasUPx2:
ID: "12345"
firstname: "Faraaz"
items:
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
type: "regular"
A couple of issues:
The pattern is that leave must be called inside the completion handler of the asynchronous call. You want this to be the last thing performed inside the closure, so you could add it as the the last line within completion handler closure.
Or I prefer to use a defer clause, so that not only do you know it will be the last thing performed in the closure, but also:
you ensure you leave even if you later add any "early exits" inside your closure; and
the enter and leave calls visually appear right next to each other in the code saving you from having to visually hunt down at the bottom of the closure to make sure it was called correctly.
You also, if you want to wait for the asynchronous calls in the for loop, have to add it there, too.
A very minor point, but you might want to not create the group until you successfully unwrapped uid. Why create the DispatchGroup if you could possibly return and not do any of the asynchronous code?
Thus, perhaps:
func fetchHistory() {
if items.count > 0 {
items.removeAll()
}
var itemNames = [String]()
guard let uid = fAuth.currentUser?.uid else {return}
let group = DispatchGroup()
group.enter()
fData.child("users").child(uid).child("items").observe(.value, with: { snapshot in
defer { group.leave() } // in case you add any early exits, this will safely capture
if snapshot.childrenCount > 0 {
...
for item in itemNames {
group.enter() // also enter before we do this secondary async call
fData.child("users").child(uid).child("items").child(item).observe(.value, with: { snapshot in
defer { group.leave() } // and, again, defer the `leave`
...
})
}
...
} else {
...
}
})
group.notify(queue: .main) {
self.booksTable.reloadData()
print("Reloading table")
}
}
While there is a brilliant answer from Rob, I would approach a solution from a different direction.
A book can only ever had one person check it out (at a time), but a borrower can have multiple books. Because of that relationship, simply combine who has the book with the book itself:
Here's a proposed users structure
users
uid_0
name: "Rob"
uid_1
name: "Bill"
and then the books node
books
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
checked_out_by: "uid_0"
check_date: "20180118"
Then to get ALL of the books that Rob has checked out and use those results to populate an array and display it in a tableview becomes super simple:
//var bookArray = [Book]() //defined as a class var
let booksRef = self.ref.child("books")
let query = booksRef.queryOrdered(byChild: "checked_out_by").queryEqual(toValue: "uid_0")
booksRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let book = Book(initWithSnap: snap) //take the fields from the snapshot and populate the book
self.bookArray.append(book)
}
self.tableView.reloadData()
})
But then you ask yourself, "self, what if I want a record of who checked out the book?"
If you need that functionality, just a slight change to the books node so we can leverage a deep query;
books
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
author: "Suzanne Collins"
coverUrl: "https://images.gr assets.com/books/1358275419s/..."
dueDate: "Date"
status: "Checked"
title: "Mockingjay (The Hunger Games, #3)"
check_out_history
"uid_0" : true
"uid_1" : true
and move the check out dates to the users node. Then you can query for any user of any book and have history of who checked out that book as well. (there would need to be logic to determine who has the book currently so this is just a starting point)
Or if you want another option, keep a separate book history node
book_history
78DFB90A-DE5B-47DE-ADCA-2DAB9D43B9C8
-9j9jasd9jasjd4 //key is created with childByAutoId
uid: "uid_0"
check_out_date: "20180118"
check_in_date: "20180122"
condition: "excellent"
-Yuhuasijdijiji //key is created with childByAutoId
uid: "uid_1"
check_out_date: "20180123"
check_in_date: "20180125"
condition: "good"
The concept is to let Firebase do the work for you instead of iterating over arrays repeatedly and having to issue dozens of calls to get the data you need. Adjusting the structure makes it much simpler to maintain and expand in the future as well - and it avoids all of the issues with asynchronous code as it's all within the closure; nice and tidy.

When retrieving data from Firebase Database, <null> is returned: "Snap (...) <null>"

I'm a relatively new Swift programmer and am using Firebase for the first time so please excuse any misunderstandings I may have and my lack of knowledge about terminology.
I am attempting to retrieve data about a user that is stored in a database (email and username).
The code successfully finds the userID in the database. The userID is then used in order to navigate into the directory containing the username and email. It stores those values in snapshot.
For some reason, when snapshot is printed, it shows the userID but the contents of the directory (username and password) are shown as <null>. I am certain that the directory I am attempting to access and retrieve data from exists and is not empty (it contains a username and email). I wantsnapshot to store the username and email, but printing shows that it is not doing so correctly and I cannot figure out why.
here is my code block:
func checkIfUserIsLoggedIn() {
if Auth.auth().currentUser?.uid == nil {
perform(#selector(handleLogout), with: nil, afterDelay: 0)
} else {
let uid = Auth.auth().currentUser?.uid;
Database.database().reference().child("Users").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot)
if let dictionary = snapshot.value as?[String:AnyObject] {
self.userLabel.text = dictionary["name"] as? String
}
}, withCancel: nil)
}
}
and here is what is being printed to the console:
Snap (ywU56lTAUhRpl3csQGI8W8WmQRf1) <null>
Here is the database entry I am attempting to reach and log to snapshot:
I'm a new Stack Overflow user and don't have enough experience on the site to be allowed to embed images in posts, so this is the external link
Thanks for reading, any help would be much appreciated!!
Your reference in Firebase is to "users", but you are using .child("Users") in your code. Make sure your lookup matches case to your node. I find it best to create a reference to that node and use it for writing to and reading from.
let usersRef = Database.Database().reference().child("users")
Snap (ywU56lTAUhRpl3csQGI8W8WmQRf1) <null> the portion in parenthesis refers to the end node of what you are trying to observe. In this case it refers to uid!.
if u want to get username or email then you make first the model class for
Example:-
class User: NSObject {
var name: String?
var email: String?
}
then user firebase methed observeSingleEvent
FIRDatabase.database().reference().child("user").child(uid).observeSingleEvent(of: .value, with: { (snapShot) in
if let dictionary = snapShot.value as? [String: Any]{
// self.navigationItem.title = dictionary["name"] as? String
let user = User()
user.setValuesForKeys(dictionary)
self.setUpNavigationBarWithUser(user: user)
}
})`
if it is not finding your asking values, you are asking wrong directory. check firebase db child name it must be exactly like in your code ("Users")

Swift Parse Query To Get All The People Whose Organization Matches The Logged In Users

On our Parse DB we have a User class and an Organization class. There's an object in the User class that points to the Organization class. I want to be able to query for all of the users with the same Organization ID as the currently logged in user.
let query = PFQuery(className: "Member")
query.whereKey("organization", equalTo: PFUser.current())
query.findObjectsInBackground {
(objects: [PFObject]?, error: Error?) -> Void in
if let objects = objects as [PFObject]? {
self.memberObject = []
for object in objects {
self.memberObject.append(object)
self.member.append(Member(
id: object.objectId!,
createdAt: object["createdAt"] as! String,
firstName: object["firstName"] as! String,
lastName: object["lastName"] as! String,
username: object["username"] as! String,
email: object["email"] as! String,
role: object["role"] as! String))
print(self.memberObject)
}
self.tableView.reloadData()
} else {
print("failure")
}
}
This is the query I currently have. So far it isn't pulling anything and nothing is being populated in the arrays. How can I check for what I'm checking for. Which is essentially to see if the logged in user shares a value with another user?