Firebase paginating data returns same data? - swift

Currently, I am initially loading the user's messages through :
func fetchMessages() {
if started == true {
let messageRef = Database.database().reference().child("messages").queryOrdered(byChild: "convoID").queryEqual(toValue: convoID).queryLimited(toLast: 10)
messageRef.observe(.childAdded) { (snapshot) in
if let value = snapshot.value as? NSDictionary {
let newMessage = message()
newMessage.messageText = value["content"] as? String
newMessage.sender = value["sender"] as? String
newMessage.messageID = snapshot.key
self.messageList.append(newMessage)
self.queryingStatus = true
self.messagesTableView.reloadData()
self.scrollToBottom()
}
}
}
}
Now, to minimize the data download, I decided to break the messages into chunks as such so that the user will download ten subsequent messages each time they pull up on the table view:
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
let lastIDDictionary = messageList[0]
let lastIDQueried = lastIDDictionary.messageID
let messageRefAddition = Database.database().reference().child("messages").queryOrdered(byChild: "convoID").queryLimited(toLast: 10).queryEnding(atValue: convoID, childKey: lastIDQueried!)
messageRefAddition.observeSingleEvent(of: .value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let messageValue = child.value as? NSDictionary {
let newMessage = message()
newMessage.messageText = messageValue["content"] as? String
newMessage.sender = messageValue["sender"] as? String
newMessage.messageID = child.key
self.messageList.insert(newMessage, at: 0)
self.messagesTableView.reloadData()
}
}
}
refreshControl.endRefreshing()
}
The problem is, when I pull up on the table view, the first time it returns some new messages (I am not sure whether the order is even correct). However, when I pull on the table view again to refresh, it adds those same ten messages again. I printed the lastIDQueried in the refresh method, and after the initial load the ID remains the same even though I am accessing the first item in the array of dictionaries? Basically,when I refresh the table view, it is not querying the correct data and my pagination implementation does not seem to be working correctly.

Basically, the problem was that I was inserting the post in the wrong place in the array and the last item was still being added to the array which was always the same (as I ended on the value). As such, I added a counter that incremented each time a value was added. Then, I inserted the subsequent post at the counter value in the array then again incremented. Finally, if the message ID was equal to the current first message in array, I would not insert it.

Related

How to read data from firebase and append it to an array

I have a userImages collection and I am trying to read data from it and appending it to my imgUrls array.
This is my code where I read the data from the database and try appending it to my array. Unfortunately, I keep getting an error because the array is apparently empty.
override func viewDidLoad() {
var ref: DatabaseReference!
let userID = Auth.auth().currentUser?.uid //holds the current user uid
ref = Database.database().reference()
var imgUrls = [String]() //array to hold the image urls from the userImages collection
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { (snapshot) in //read from userImages collection only from the subcollection where the
guard let dict = snapshot.value as? [String: Any] else { return } //document Id equals the current user uid. Create a dictionary from the
//snapshot values
let values = dict.values //holds the values from the dictionary
for value in values { //for loop to go through each value from the dictionary
imgUrls.append((value as? String)!) //and append to the imgUrls array
}
}
testLabel.text = imgUrls[0] //I used this to test, but i get an error saying the array is empty
}
I posted a question before, but it was so convoluted I decided to delete it and repost it simpler.
Any help is much appreciated!
the reason you are not getting anything in your testLabel.text is because:
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { (snapshot) in //read from userImages collection only from the subcollection where the
...
}
is an asynchronous function called. That is it will be done sometimes in the future.
but your:
testLabel.text = imgUrls[0] //I used this to test, but i get an error saying the array is empty
is outside of this call. So the results in "imgUrls" are not available yet.
Put this line inside the function or wait until it has finished before using the results.
You try to use the array before the observeSingleEvent closure is executed. All observation calls to Firebase are asynchronous. This means that you test code is executed before the closure and the array is still empty.
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
// this closure is executed later
guard let dict = snapshot.value as? [String: Any] else { return }
let values = dict.values
for value in values {
imgUrls.append((value as? String)!)
}
// your test code should bee here
}
// this is executed before closure and the array is empty
testLabel.text = imgUrls[0]
For this reason you get the error. You need to add your test code into the closure.
However, there is another catch. Asynchronous calls (their closures) are executed on the background thread.
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
// this is executed on the background thread
}
However, all user interface calls must be executed on the main thread. Also wrap your test code by calling the main thread, otherwise you won't see the result in the user interface.
DispatchQueue.main.async {
// this code is executed on the main thread
// all UI code must be executed on the main thread
}
After editing, your code might look like this:
override func viewDidLoad() {
var ref: DatabaseReference!
let userID = Auth.auth().currentUser?.lid
ref = Database.database().reference()
var imgUrls = [String]()
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
guard let dict = snapshot.value as? [String: Any] else { return }
let values = dict.values
for value in values {
imgUrls.append((value as? String)!)
}
DispatchQueue.main.async {
testLabel.text = imgUrls[0]
}
}
}

How to save data to a specific uid for a specific logged in user in swift/firebase

I'm trying to allow a user to save data to a tableView using an alert that transfer data from the View Controller that the alert is in (CreatePlaylistVC) to another ViewController(CreatedPlaylistVC) that the tableView is in, saving for each specific account for a specific uid.
I've tried setting the value to the uid but this did work for me although it did save to the database under that specific uid.
CreatePlaylistVC
ref = Database.database().reference()
alert.addAction(UIAlertAction(title:"OK", style:.default, handler: {
action in
if let playlistName = alert.textFields?.first?.text {
let userID = Auth.auth().currentUser?.uid
self.ref?.child("PlaylistName").child(userID!).setValue(playlistName)
CreatedPlaylistVC
var ref:DatabaseReference?
var databaseHandle:DatabaseHandle?
override func viewDidLoad() {
//Set the firebase reference
ref = Database.database().reference()
//Retrieve the posts and listen fro changes
databaseHandle = ref?.child("PlaylistName").observe(.childAdded, with: { (snapshot) in
//Try to covert the value of the data to a string
let post = snapshot.value as? String
if let actualPost = post {
//Append the data to our playlistNameArray
self.playlistNameArray.append(actualPost)
//Reload the tableView
self.tableView.reloadData()
}
})
}
The expected results is to save the data only for the specified uid or currently logged in user. But it is saving for each user even though in the database it is saved to the right uid.
When using .childAdded for observe, it will go through every existing child under "PlaylistName" which in this case will be every user that has saved something.
Might have to reconsider your structure. Or use childByAutoID.
Edit: To use child by auto ID
// Your ["Name": Playlist] will get nested into an autogenerated child
self.ref?.child("PlaylistName").child(userID!).childByAutoID().setValue(["Name":playlistName])
//You will be listening for any new additions that your current user has made
databaseHandle = ref?.child("PlaylistName").child(Auth.auth().currentUser!.uid).observe(.childAdded, with: { (snapshot) in
// this loops through a list of playlist names your user creates
for child in snapshot.children{
let snap = child as! DataSnapshot
let value = snap.value as? Dict<String,Any>
let post = value["Name"] as! String
if let actualPost = post {
self.playlistNameArray.append(actualPost)
self.tableView.reloadData()
}
}
})
But my suggestion, if it suits your application, would be to change your data structure because it can keep your structure a lot flatter.
self.ref?.child(userID!).child("PlaylistName").setValue(playListName)
databaseHandle = ref?.child(userID!).observe(.childAdded, with: { (snapshot) in
let value = snapshot.value as? Dict<String,Any>
let post = value["PlaylistName"]
if let actualPost = post {
self.playlistNameArray.append(actualPost)
self.tableView.reloadData()
}
})

Item keeps getting added to Tableview even if I just have one in my DB

I have this code and I call it every time I click on a button and call another viewcontroller where I call fetchVinylData inside viewWillappear. The problem is everytime I click a button to go to this VC it adds one item to the tableview even if I only have one inside my database.I am guessing is because my array keeps getting fed even if there is only one record in my database.How do I delete from my array so I don't get many values inside my tableView only the ones that is saved on my firebase database?I tried to add myArray.removeAll() inside fetchrequest before I load the vinyl to the array but my app crashs eventually
func fetchVinylData() {
SVProgressHUD.show()
guard let currentUID = Auth.auth().currentUser?.uid else { return }
dbRef.child("vinylsOUT").child(currentUID).observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? Dictionary<String,AnyObject> else { return }
let vinyl = Vinyl(dictionary: dictionary)
self.vinyls.append(vinyl)
self.vinyls.sort(by: { (vinyl1, vinyl2) -> Bool in
return vinyl1.artist < vinyl2.artist
})
self.tableView.reloadData()
}
SVProgressHUD.dismiss()
}
thank yo very much
You are appending to your array every time you get back data from Firebase:
- self.vinyls.append(vinyl)
You can just override your current vinyls array by doing:
self.vinyls = [Vinyl]()
before you fetch the new data from Firebase.
That would look like this:
func fetchVinylData() {
SVProgressHUD.show()
guard let currentUID = Auth.auth().currentUser?.uid else { return }
self.vinyls = [Vinyl]() // <- here you reset the array
dbRef.child("vinylsOUT").child(currentUID).observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? Dictionary<String,AnyObject> else { return }
let vinyl = Vinyl(dictionary: dictionary)
self.vinyls.append(vinyl)
self.vinyls.sort(by: { (vinyl1, vinyl2) -> Bool in
return vinyl1.artist < vinyl2.artist
})
self.tableView.reloadData()
}
SVProgressHUD.dismiss()
}

Order tableviewcell by child timestamp

I tried with this code to sort my posts by timestamp it doesn't work, each time I launch the simulator the order of the cells is different, I suppose this isn't the way to do it, could somebody explain me where I am wrong...
I edited the code, now my problem is that the most recent posts are displayed at the bottom and I would like them to to be displayed at the top
self.user.removeAll()
for child in DataSnapshot.children.allObjects as! [DataSnapshot] {
print("Processing user \(child.key)")
let value = child.value as? NSDictionary
//if country == "UNITED STATES"{
if let uid = value?["userID"] as? String{
if uid != Auth.auth().currentUser!.uid {
//
let userToShow = User()
if let fullName = value?["username"] as? String , let imagePath = value?["photoURL"] as? String{
userToShow.username = fullName
userToShow.imagePath = imagePath
userToShow.userID = uid
self.user.append(userToShow)
}
}
}
}
As soon as you call DataSnapshot.value, you're converting the data in the snapshot into a dictionary. And the order if keys in that dictionary is not guaranteed.
To maintain the order of the elements as they come back from the database, you need to loop over DataSnapshot.children. See these questions for examples of how to do that:
Iterate over snapshot children in Firebase
post on the firebase-talk mailing list
For your code this would look something like:
ref.child("users").queryOrdered(byChild: "timestamp").observeSingleEvent(of: .value, with: { snapshot in
self.user.removeAll()
for child in snapshot.children.allObjects as [DataSnapshot] {
print("Processing user \(child.key)")
let value = child.value as? NSDictionary
if let uid = value["userID"] as? String {
...
}
}
self.tableview.reloadData()
})

how to read firebase database

This is my data structure: many clubs, each club has address. I tried to make the database flat.
Now I want to load a few club info on table view. When I swipe down iPhone screen, it will load next a few club info.
This is my code. But it loads all club info. How can I load only a few club, and load next a few club when user swipe down?
func loadClubs() {
ref = Database.database().reference()
ref.child("club").observe(DataEventType.value, with: { (snapshot) in
//print("clubs: \(snapshot)")
let array:NSArray = snapshot.children.allObjects as NSArray
for obj in array {
let snapshot:DataSnapshot = obj as! DataSnapshot
if let childSnapshot = snapshot.value as? [String: AnyObject] {
if let clubName = childSnapshot["name"] as? String {
print(clubName)
}
}
}
})
}
Firebase's queries support pagination, but it's slightly different from what you're used to. Instead of working with offsets, Firebase uses so-called anchor values to determine where to start.
Getting the items for the first page is easy, you just specify a limit to the number of items to retrieve:
ref = Database.database().reference()
query = ref.child("club").queryOrderedByKey().limitToFirst(10)
query.observe(DataEventType.value, with: { (snapshot) in
Now within the block, keep track of the key of the last item you've shown to the user:
for obj in array {
let snapshot:DataSnapshot = obj as! DataSnapshot
if let childSnapshot = snapshot.value as? [String: AnyObject] {
lastKey = childSnapshot.key
if let clubName = childSnapshot["name"] as? String {
print(clubName)
}
}
}
Then to get the next page, construct a query that starts at the last key you've seen:
query = ref.child("club").queryOrderedByKey().startAt(lastKey).limitToFirst(11)
You'll need to retrieve one more item than your page size, since the anchor item is retrieve in both pages.
i think the right way make reference to array of elements,
and make variable for index
var i = 0;
var club = null;
club = loadClubs(index); // here should return club with specified index;
// and increment index in loadClubs func,
// also if you need steped back --- and one more
// argument to function, for example
// loadClubs(index, forward) // where forward is
// boolean that says in which way we should
// increment or decrement our index
so in your example will be something like this:
func loadClubs(index, forward) {
ref = Database.database().reference()
ref.child("club").observe(data => {
var club = data[index];
if(forward){
index ++
}else{
index--
}
return club;
})
}