Swift firebase pagination - swift

I am working on my pagination feature and so far was able to paginate posts by timestamp.
The issue is in the version I use (from a course) that it queries posts depending on its value always starting from a value a bit higher than before which works fine when using timestamp as the numbers are really high and more likely to differ.
Now I want to do the pagination stuff with my posts ordered by likeCount. The issue now is that as there are many posts/comments with equal likes the pagination doesnt work properly anymore.
Here Is my code
func observeAllPostsForTrendByLikeCount(start likeCount: Int? = nil, limit: UInt, completionHandler: #escaping ([(Post, UserModel)]) -> Void) {
var postQuery = REF_POST.queryOrdered(byChild: "likeCount")
if let latestPostLikeCount = likeCount, latestPostLikeCount >= 0 {
postQuery = postQuery.queryStarting(atValue: latestPostLikeCount + 1, childKey: "likeCount").queryLimited(toLast: limit)
} else {
postQuery = postQuery.queryLimited(toLast: limit)
}
postQuery.observeSingleEvent(of: .value, with: { (snapshot) in
let items = snapshot.children.allObjects
let myGroup = DispatchGroup()
var results: [(post: Post, user: UserModel)] = []
for (index, item) in (items as! [DataSnapshot]).enumerated() {
myGroup.enter()
API.Post.observePost(withId: item.key, completion: { (post) in
API.User.observeUser(withId: post.userId!, completion: { (user) in
results.insert((post, user), at: index)
myGroup.leave()
})
})
}
myGroup.notify(queue: .main) {
results.sort(by: {$0.0.likeCount! > $1.0.likeCount!})
completionHandler(results)
}
})
}
func loadMoreTrendByLikeCount(start likeCount: Int, limit: UInt, completionHandler: #escaping ([(Post, UserModel)]) -> Void) {
let postOrderedQuery = REF_POST.queryOrdered(byChild: "likeCount")
let postLimitedQuery = postOrderedQuery.queryEnding(atValue: likeCount , childKey: "likeCount").queryLimited(toLast: limit)
postLimitedQuery.observeSingleEvent(of: .value, with: { (snapshot) in
let items = snapshot.children.allObjects
let myGroup = DispatchGroup()
var results = [(post: Post, user: UserModel)]()
for (index, item) in (items as! [DataSnapshot]).enumerated() {
myGroup.enter()
API.Post.observePost(withId: item.key, completion: { (post) in
API.User.observeUser(withId: post.userId!, completion: { (user) in
results.insert((post, user), at: index)
myGroup.leave()
})
})
}
myGroup.notify(queue: .main) {
results.sort(by: {$0.0.likeCount! > $1.0.likeCount! })
completionHandler(results)
}
})
}
This code works fine as long as the posts have different like counts but as I scroll deeper and face multiply posts with 0 likes it doesn't load further.
here is how I fetch the posts
func loadTrendByLikeCount() {
isLoadingPost = true
API.Post.observeAllPostsForTrendByLikeCount(start: posts.first?.likeCount, limit: 3) { (results) in
if results.count > 0 {
results.forEach({ (result) in
if self.segmentedControl.selectedSegmentIndex == 1 {
self.posts.append(result.0)
self.users.append(result.1)
}
})
}
self.isLoadingPost = false
if self.refreshControl.isRefreshing {
self.refreshControl.endRefreshing()
}
self.activityIndicatorView.stopAnimating()
self.tableView.reloadData()
}
}
and the scroll method
if scrollView.contentOffset.y >= scrollView.contentSize.height - self.view.frame.size.height {
guard !isLoadingPost else {
return
}
isLoadingPost = true
guard let lastPostLikeCount = self.posts.last?.likeCount else {
isLoadingPost = false
return
}
self.Indicator.startAnimating()
API.Post.loadMoreTrendByLikeCount start: lastPostLikeCount, limit: 4) { (results) in
if results.count == 0 {
self.Indicator.stopAnimating()
return
}
for result in results {
if !self.posts.contains(where: {$0.id == result.0.id}) {
self.posts.append(result.0)
self.users.append(result.1)
}
}
self.tableView.reloadData()
self.Indicator.stopAnimating()
self.isLoadingPost = false
}
}
As I said this works and loads more post as long as not too many posts are at the same likecount.
Moreover the loadMore() function ends in an infinity loop...
Thanks in advance

So first let me explain what is happening here. When you call loadMoreTrendByLikeCount you provide a starting like count let's say 10 likes in this example and you are asking Firebase to get the next limit number of posts. In this example let's assume the limit is 5 and we have the following posts:
"posts": {
"key-1" : {
"likes" : 10,
"timestamp" : 1514852613107,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-2" : {
"likes" : 10,
"timestamp" : 1515369388635,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-3" : {
"likes" : 10,
"timestamp" : 1515538024957,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-4" : {
"likes" : 10,
"timestamp" : 1515700542687,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-5" : {
"likes" : 10,
"timestamp" : 1515704869452,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-6" : {
"likes" : 10,
"timestamp" : 1515704970865,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-7" : {
"likes" : 10,
"timestamp" : 1515773432428,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-8" : {
"likes" : 10,
"timestamp" : 1515872606809,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
},
"key-9" : {
"likes" : 0,
"timestamp" : 1515970044560,
"userID" : "iijhsPlIxbWrdQty5nn9IUAZRY63"
}
}
Now we perform the following query: loadMoreTrendByLikeCount(start likeCount: 10, limit: 5) We will get the following posts key-8, key-7, key-6, key-5, key-4. When you query again you will get the same 5 posts because the query starts in the same place. (i.e. your limit is not large enough to get all posts with 10 likes, thus the starting position stays the same)
Now, I am not sure how to resolve this issue. I have only ever used pagination with the autoID keys which are unique so this issue cannot occur. The only thing I can think of is to detect that you are querying the same posts again and increase the limit so you get new posts. You'll have to keep increasing this limit until you get past all the posts with the same number of likes. Depending on the number of posts this can take awhile and be very inefficient.
Ideally, Firebase would support querying all posts between say 10-11 likes to avoid this problem, but that isn't possible. It looks like you have to somehow store another value related to the likes that allows you to filter as desired (for example if you had "filterLikes": 10.0, "filterLikes": 10.1, "filterLikes": 10.2, etc) . Take a look at this question it might be helpful.

Related

Firebase query returns wrong items

Here is my database structure:
"current" : {
"-MPcUEyEdXiotnBybJFM" : {
"completed" : 123456789,
"completedBy" : "Mom",
"name" : "🍼 Babysit (2 hours)",
"repeats" : 12,
"startDate" : 1609144537,
"value" : 884
},
"-MPcUEyF9YsmQrGpt78Q" : {
"completed" : 1609144538,
"completedBy" : "Mom",
"name" : "🛠 Organize garage",
"repeats" : 6,
"startDate" : 1609144538,
"value" : 884
},
"-MPkV1sOlx3wNS8UvEqQ" : {
"name" : "🕸 Dust for cobwebs",
"repeats" : 1,
"startDate" : 1611957359,
"value" : 221
}
}
And here is my Firebase query:
static func deleteOldCompletedItems(completion: #escaping () -> Void) {
let previousPayday = FamilyData.calculatePayday().previous.timeIntervalSince1970
FB.ref
.child(FB.jobJar)
.child(FB.current)
.queryOrdered(byChild: FB.completed)
.queryEnding(atValue: previousPayday)
.observeSingleEvent(of: .value, with: { (snapshot) in
print(previousPayday)
guard snapshot.exists() else {
completion()
return
}
var deletedItemsCount = 0
for item in snapshot.children {
guard let snap = item as? DataSnapshot else { return }
snap.ref.removeValue { (_, _) in
deletedItemsCount += 1
if deletedItemsCount == snapshot.childrenCount {
completion()
}
}
}
})
}
And here is the data model:
struct JobJar {
var name: String
var value: Int
var repeats: Int
var startDate: Double
var completed: Double?
var completedBy: String?
}
Note that two of the parameters are optional: completed: and completedBy:.
I want to only get the items from my database that match the completed: timestamp and that are more than a week old. Then I want to delete those items.
The code works fine for items that have a completed: parameter.
But for some reason, my query is returning items that don't have the completed: timestamp. In other words, my query is deleting the items that are more than a week old (good!) but also all the other items that don't have any timestamp. It's deleting too much stuff.
Why? Is it because the completed: parameter is optional?
How do I query the database so that I ONLY get items that have the completed: parameter that are more than a week old?
I'm surprised that the database returns items that don't have the property you order on. But as a workaround, you can probably add a queryStarting(atValue: 1) to the query.

Counting Records In Swift From Firebase With A Where Statement

I am trying to return the count of posts for a specific user where a field contains a certain value. The structure I am using in Firebase is here:
{
"posts" : {
"-Lyzpsb1hH4LUPvcXE3H" : {
"beverageCategory" : "Beer",
"beverageName" : "Dortmunder",
"beveragePrice" : "3.99",
"beverageRating" : 3,
"beverageType" : "Lager",
"imageUrl" : "https://firebasestorage.googleapis.com/v0/b/socialspirit-37cae.appspot.com/o/post-pics%2F3C23E923-A3BD-4393-B3EF-8F939D8B08B8?alt=media&token=ac80cb98-f2a1-4197-b328-2a69a2ac1fd1",
"wineVintage" : ""
}
},
"user" : "sdkjvksdjf",
"users" : {
"0hyN8N2klJWqPi2lkRqNK3vg7z63" : {
"posts" : {
"-Ly6ciYHm7v5JFy1VmVY" : true,
"-Ly6cyLukI6aRRki5yna" : true
},
"provider" : "Firebase"
},
"26OUuaRZEVWUmkLJp13LzirGbs13" : {
"posts" : {
"-Ly6fGmQEMZz3c-azMnJ" : true
},
"provider" : "Firebase"
},
"5r6FulsvIRap7pLK5D3zV8qyPVv1" : {
"provider" : "Firebase"
},
"gQpktBMh97hTqiysHBwvVLZl70y1" : {
"posts" : {
"-Lyzpsb1hH4LUPvcXE3H" : true,
"-LyzqFgvmrBgdsgKSt_5" : true,
"-Lz-fIMDal00ex3_viQo" : true,
"-Lz-lXsSBqKlcf8hBext" : true,
"-Lz2eNLfk1PFEVkEgmwa" : true,
"-Lz2etF0UqFqLkdGOR13" : true,
"-Lz2fGUi0qzJtniNr5LX" : true,
"-Lz2gP5c47yHDO2g0ljr" : true,
"-Lz2gdhfedZfTtzjP2ee" : true,
"-Lz2gllC3caXgPf2VAPU" : true,
"-Lz2hH8FbNmBEOLcHCJz" : true,
"-Lz2jzkXuEZ3Cfe96eGW" : true,
"-Lz3RvuW4fMXarhW7vLv" : true,
"-Lz3S8YA0vgk4ZrjL_Kk" : true
},
"provider" : "Firebase"
}
}
}
I am using the following code to get the count of records that have a beverageCategory of "Beer".
DataService.ds.REF_USERS.child("\(uid)").child("posts").child("beverageCategory")
.queryOrderedByValue().queryEqual(toValue: "Beer")
.observe(DataEventType.value, with: { (snapshot) in
print("SNAP - \(snapshot.childrenCount)")
})
The childrenCount returns 0, however. Any idea what I'm doing wrong here? Thanks!
EDIT: I added the JSON for the data structure and I think I left an important detail out. The data structure actually has users that have posts and the associated postid that links back to posts. I think what I have to do is first find the user's posts and then of those posts get a total count for each category. I'm guessing that is slightly different than the methods I tried. Any ideas on that?
EDIT 2: So my issues is that users and posts are in different structures. I need to find all of the user's posts and iterate through them to see if what the beverageCategory is. I have done that with the following code. The issue with the code below is that the count is doubled when I add a post and then open the menu. But when I exit the menu and re-open the count is correct. Why is it double-counting?
func myFirebaseNetworkDataRequest(finished: #escaping () -> Void) {
beerCountArray.removeAll()
wineCountArray.removeAll()
liquorCountArray.removeAll()
print("BEER ARRAY \(beerCountArray.count)")
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let newPost = DataService.ds.REF_USERS.child("\(uid)").child("posts")
newPost.observe(.value, with: { (snapshot) in
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
//print("SNAPSHOT - \(snapshot)")
for snap in snapshot {
let postData = DataService.ds.REF_POSTS.child(snap.key)
//print("SNAP KEY - \(snap.key)")
let bevCat = DataService.ds.REF_POSTS.child(snap.key).child("beverageCategory")
//print("BEV CAT - \(bevCat)")
postData.observe(.value, with: { (snapshot) in
if let postDict = snapshot.value as? Dictionary<String, AnyObject> {
let key = snapshot.key
let post = Post(postKey: key, postData: postDict)
//print("POST DICT - \(String(describing: postDict["beverageCategory"]!))")
if postDict["beverageCategory"]! as! String == "Beer" {
self.beerCountArray.append(1)
//print("BEER ARRAY LOOP - \(self.beerCountArray)")
}
if postDict["beverageCategory"]! as! String == "Wine"{
self.wineCountArray.append(1)
}
if postDict["beverageCategory"]! as! String == "Liquor" {
self.liquorCountArray.append(1)
}
//self.posts.append(post)
}
finished()
})
}
}
})
}
And...
override func viewDidLoad() {
super.viewDidLoad()
beerCountArray.removeAll()
wineCountArray.removeAll()
liquorCountArray.removeAll()
myFirebaseNetworkDataRequest {
//print("BEER ARRAY - \(self.beerCountArray.count)")
self.beerCount.text = String(self.beerCountArray.count)
self.liquorCount.text = String(self.liquorCountArray.count)
self.wineCount.text = String(self.wineCountArray.count)
}
}
Your query is not correct. Since you're trying to filter on a child property of each node under posts, you should call queryOrdered(byChild:).
So:
DataService.ds.REF_USERS.child("posts")
.queryOrdered(byChild: "beverageCategory")
.queryEqual(toValue: "Beer")
.observe(DataEventType.value, with: { (snapshot) in
print("SNAP - \(snapshot.childrenCount)")
})
Also see the Firebase documentation on ordering and filtering data.

Firebase gives sometimes only parts of my saved values and sometimes everything

I'm trying to code my own messenger. I saved the messages in Firebase database. I'm trying to access the messages with the normal way how I do it every time (database.database().reference().child("users")...., but it gives back only a few messages and only sometimes every message
I already tried it with .childAdded but it doesn't work either
func fetchMessages() {
self.messages.removeAll()
let uid = Auth.auth().currentUser!.uid
let ref = Database.database().reference().child("users").child(uid).child("chats").child(self.chatuid).child("messages")
ref.observeSingleEvent(of: .value) { (snap) in
guard let data = snap.value as? [String: AnyObject] else { return }
//MARK: data = (messageID, AnyObject)
for (messageID, _) in data {
ref.child(messageID).observeSingleEvent(of: .value) { (snap2) in
guard let data2 = snap2.value as? [String: AnyObject] else { return }
//MARK: data2 = ("message": String, "sentuid": String)
guard let message = data2["message"] as? String else { return }
guard let sentuid = data2["sentuid"] as? String else { return }
let messageToAppend = Message(sentuid: sentuid, message: message)
self.messages.append(messageToAppend)
self.messageTableView.reloadData()
}
}
}
}
//self.messages = Place where I save my fetched messages
Firebase-Structure:
{
"users" : {
"V3bLZu61KYTCG0uCWV8LH1f4rRJ3" : {
"chats" : {
"x3GzxwmyLbXrQNNyBF2a82KqrHa2" : {
"messages" : {
"20191014165454" : {
"message" : "Heyho",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165647" : {
"message" : "Blabla",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165754" : {
"message" : "Blabla",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165800" : {
"message" : "Fuchsloch",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165805" : {
"message" : "Jlksaö",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165808" : {
"message" : "Jadfsk",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165811" : {
"message" : "Jskldflsö",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014170041" : {
"message" : "JALSF",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"test" : {
"messange" : "test",
"sentUid" : "MatesForSports"
}
}
}
},
"username" : "Acc2"
},
"x3GzxwmyLbXrQNNyBF2a82KqrHa2" : {
"chats" : {
"V3bLZu61KYTCG0uCWV8LH1f4rRJ3" : {
"messages" : {
"20191014165454" : {
"message" : "Heyho",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165647" : {
"message" : "Blabla",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165754" : {
"message" : "Blabla",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165800" : {
"message" : "Fuchsloch",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165805" : {
"message" : "Jlksaö",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165808" : {
"message" : "Jadfsk",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014165811" : {
"message" : "Jskldflsö",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"20191014170041" : {
"message" : "JALSF",
"sentuid" : "V3bLZu61KYTCG0uCWV8LH1f4rRJ3"
},
"test" : {
"messange" : "test",
"sentUid" : "MatesForSports"
}
}
}
},
"username" : "Acc1"
}
}
}
I expected that the code would give back around 10 messages, but it gives sometimes 5, sometimes 3 or even only 2 messages
My firebase-structure of the messages is .child("messages"), then a unique id, then the actual message and the sender at one layer
Here's an an answer followed by a suggestion.
Let's start with replicating your structure using some hard coded values
users
uid_0
chats
chat_uid_0
messages
unique_id_0
msg: "My Message"
sent_uid: "uid_1"
unique_id_1
msg: "Hello, World"
sent_uid: "uid_3"
unique_id_2
msg: "another message"
sent_uid: "uid_2"
then the code to read in the messages, iterate over them and print out the msg and sent_uid
func readMessages() {
let usersRef = self.ref.child("users")
let messagesRef = usersRef.child("uid_0").child("chats").child("chat_uid_0").child("messages")
messagesRef.observeSingleEvent(of: .value, with: { snapshot in
let allMessages = snapshot.children.allObjects as! [DataSnapshot]
for msg in allMessages {
let msgText = msg.childSnapshot(forPath: "msg").value as? String ?? "No Msg"
let sentUid = msg.childSnapshot(forPath: "sent_uid").value as? String ?? "No sender"
print(msgText, sentUid)
}
})
}
and the output is
My message uid_1
Hello, World uid_3
another message uid_2
That being said - your structure is way to deep and should be denormalized. While it may work, as soon as you want to query for items, of find messages posted by a specific user, it just doesn't work.
I don't know your full use case but this would be a better structure
users
uid_0
name: "Hank"
uid_1
name: "Leroy"
chats
uid_0
chat_id_0: true
chat_id_1: true
uid_1
chat_id_5: true
chat_id_0
unique_id_0
msg: "My Message"
sent_uid: "uid_1"
unique_id_1
msg: "Hello, World"
sent_uid: "uid_3"
unique_id_2
msg: "another message"
sent_uid: "uid_2"
With this structure, it's all very shallow and easy to get to data - perform queries etc. You can quickly retreive all chats that uid_0 is involved with, get any messages that uid_3 ever posted or add an observer to uid_1's chat so others are notified when a chat has been added. Again, this is just an example and may not fit your case but it's something to consider.

Is it possible to get database reference without childByAutoId (). Key?

Is there a way to dynamically get a child database from the parent instance, without having to use childByAutoId () ?
{
"artists" : {
"artists_id_1" : {
"name" : "Atif Aslam",
"genre" : "Rock"
},
"artists_id_2" : {
"name" : "Arijit Singh",
"genre" : "Rock"
}
}
}
we usually refer to the path, and listener the items the DataBase...Example:
Database.database().reference().child("artists").child("artist_id_1").observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
print("\(String(describing: dictionary["name"] as? String))")
}
}, withCancel: nil)
it would only work for the first item. Because, it would not be dynamic. What is normally used is the following example below ...
{
"artists" : {
"-KeZUDrJv555kteAcssL-" : {
"name" : "Atif Aslam",
"genre" : "Rock"
},
"-KeZUVXFIQdO7JiyRYk-" : {
"name" : "Arijit Singh",
"genre" : "Rock"
}
}
}
Database.database().reference().child("artists").childByAutoId().observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
print("\(String(describing: dictionary["name"] as? String))")
}
}, withCancel: nil)
Anyway, I do not know if it was clear, anything can comment before answering. Thank you
To display all artists in your database, load the artists node and loop over snapshot.children:
Database.database().reference().child("artists").observe(.value) { (snap:DataSnapshot) in
for child in snapshot.children {
print(child.key)
}
}
For this and more examples, I recommend reading the Firebase documentation, specifically the section on reading and writing lists.
I dont know if i understand your question correctly or what you are trying to do but maybe you can try
Database.database().reference().child("artists").observe(.value) { (snap:DataSnapshot) in
// This should print out your childIds
print(snap.key)
// This should print out the values after them
print(snap.value)
}

Trouble trying to retrieve data from Firebase

I have this piece of working code that saves data to Firebase:
let locRef = locationRef.childByAutoId()
let locItem = [
senderId : [
"location": getLocationID()
]
]
locRef.setValue(locItem)
And I want to retrieve the user's (identified by senderID) "location", so I tried this piece of code:
locationRef.child("location").child(senderId).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
self.locationId = snapshot.value!["location"] as! String
}) { (error) in
print(error.localizedDescription)
}
However my app is crashing when the piece of code is run, and there are no errors. I think my mistake may be that .child("location") must be something else, but I do not know what.
Database structure (JSON):
{
"locations" : {
"-KLEdoj2eiF7EW9m0815" : {
"W6vSOHZLTwNM33JYqkKHvaIVRF13" : {
"location" : "Seattle, WA"
}
},
"-KLLfcOvYHwIufBALM0-" : {
"W6vSOHZLTwNM33JYqkKHvaIVRF13" : {
"location" : "London, United Kingdom"
}
},
Any help would be appreciated thanks!
You should be referring to your location as the following
FIRDatabase.database().reference().child("locations").child(locationId).child(senderId).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
self.location = snapshot.value!["location"] as! String
}
For example:
If you have locationId = "-KLEdoj2eiF7EW9m0815" and senderId = "W6vSOHZLTwNM33JYqkKHvaIVRF13" the above call will set the self.location to "Seattle, WA"