Efficiently refreshing TableView? - ios

I'm reading data from my NoSQL Firebase database, parsing that data into individual components, then displaying them in my tableView. I've added table refreshing functionality so when a new piece of data is added the user can refresh and it will be added to the table.
The function that's call to refresh the table is the same function that does the initial table populating, so in a sense refreshing just restarts the view. The steps that are taken are:
Empty out array and dictionary that hold parsed data elements
Fetch data from database
Parse that data
Reload the table
Here's the full function:
func readEventsFromDb() {
// 1. Empty out data structures
eventsForDate.removeAll()
allDates.removeAll()
// 2. Fetch data
let dbRef = FIRDatabase.database().reference().child("pets").child(currentPet).child("events")
dbRef.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
// 3. Parse data elements
for child in snapshots{
if let data = child.value as? [String: Any] {
if let c = data["comment"] as? String, let p = data["user"] as? String, let t = data["type"] as? Int, let d = data["date"] as? UInt64 {
let event = PetEvent(comment: c, person: p, type: t, time: self.timeFromEpoch(time: Double(d)))
let eventDate = self.dateFromEpoch(time: Double(d))
if (self.eventsForDate[eventDate] != nil) {
self.eventsForDate[eventDate]!.append(event)
} else {
self.eventsForDate[eventDate] = [event]
}
}
}
}
// 4. Refresh table
self.allDates = Array(self.eventsForDate.keys)
self.feedTable.reloadData()
self.refreshControl.endRefreshing()
}
})
}
It doesn't make a lot of sense to me that refreshing the table would pretty much just restart the view, as this is the only thing in the view. Is this how table refreshing usually works or is there a more efficient way to do such a thing?

Use ref.observe instead of ref.observeSingleEvent to continuous updating the table view.
ref.observe(.childAdded ...) //insert row
ref.observe(.childRemoved ...) //remove row
ref.observe(.childChanged ...) //update row

I'm not sure why you would do so much manual work to have the user refresh this data - one of the biggest values of Firebase is that you can do this automatically. This can work, but is definitely not how "most other applications" do this.
I would STRONGLY recommend you take a look at the FirebaseUI project:
https://github.com/firebase/FirebaseUI-iOS
This includes data sources for UITableView and UICollectionView displays that handle 90% of the work behind what you're doing, but also support incremental (and animated, like other iOS apps) row display. If a row is deleted, for instance, the user would see that deletion with a nice animation, while maintaining their scroll position within the table. (The solution you've outlined will lose this position, which isn't very user-friendly.)
Included in the project is a simple example app that uses the module to show a simple live table:
https://github.com/firebase/FirebaseUI-iOS/tree/master/FirebaseDatabaseUITests

Related

After reading data from Firestore cells in tableView randomly reorder

I'm working on application that prints name of place that has been visited according to its ID. I'm storing data in several nodes node called "placesExploredByUsers/userID" stores data about IDs of places that user have visited before and node "databaseOfPlaces" stores all IDs of places with additional info (name, location, coordinates etc.), so it works like foreign key in SQL. All of my data is stored in Firebase.
But, I'm having problem with ordering my cells in tableView in Swift. I've tried several options and this is my last modification. The problem is, that my cells are randomly reordering everytime I come to the viewController with tableView.
This is function, that should handle it.
Can anyone please help me? Thanks in advance
func getDataToTable(){
// setting the firebase reference
ref = Database.database().reference()
let userID = (Auth.auth().currentUser?.uid)!
// getting information about places that user has visited
Database.database().reference().child("placesExploredByUsers").child(userID).queryOrderedByValue().observeSingleEvent(of: .value) { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject]{
// creating array of all ID of places that user has visited
let idsOfAllPlaces = Array(dictionary.keys)
for id in idsOfAllPlaces {
// getting information about places from database of ID of all places that are in app
Database.database().reference().child("databaseOfPlaces").child(id).queryOrderedByValue().observeSingleEvent(of: .value) { (snapshot1) in
if let dictionary = snapshot1.value as? [String: AnyObject]{
// getting information about name
self.random = dictionary["name"]! as! String
// updating the table view
self.postData.append(self.random)
self.sortedList(array: self.postData)
}
}
}
}
}
}
as per documentation
By default, a query retrieves all documents that satisfy the query in
ascending order by document ID. You can specify the sort order for
your data using orderBy(), and you can limit the number of documents
retrieved using limit().
Note: An orderBy() clause also filters for existence of the given field. The result set will not include documents that do not contain the given field.
So use order(by: "databaseOfPlaces")
func getDataToTable(){
// setting the firebase reference
ref = Database.database().reference()
let userID = (Auth.auth().currentUser?.uid)!
// getting information about places that user has visited
Database.database().reference().child("placesExploredByUsers").child(userID).order(by: "databaseOfPlaces").observeSingleEvent(of: .value) { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject]{
// creating array of all ID of places that user has visited
let idsOfAllPlaces = Array(dictionary.keys)
for id in idsOfAllPlaces {
// getting information about places from database of ID of all places that are in app
Database.database().reference().child("databaseOfPlaces").child(id).queryOrderedByValue().observeSingleEvent(of: .value) { (snapshot1) in
if let dictionary = snapshot1.value as? [String: AnyObject]{
// getting information about name
self.random = dictionary["name"]! as! String
// updating the table view
self.postData.append(self.random)
self.sortedList(array: self.postData)
}
}
}
}
}
}
The problem is that you call snapshot1.value as? [String: AnyObject], which means the order is lost (because items in a dictionary have no defined order).
To process the results in the order you requested them, loop over snapshot1.children. Something like:
for snap in snap1.children.allObjects as! [DataSnapshot] {
self.random = snap.value["name"]! as! String
self.postData.append(self.random)
self.sortedList(array: self.postData)
}
Also see:
Firebase getting data in order
iOS - Firebase Filter query
How to properly use queryOrderedByValue

Retrieve child by auto id value in firebase (TableView)

I have a UI table view and when I swipe the cell I delete the cell, but not the value in firebase. I have searched everywhere but cannot find it. How can I delete the array I swiped on from the cell in my firebase. Below is how I set up my firebase database. The array I'm trying to delete is the corresponding one in the cell e.g. it may be "-L4BMZBIcYMp_f2LDMbp" etc.
Also the code I have to delete the cell and where I would find the array corresponding to the cell
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let fliper = self.flipList[indexPath.row]
if let itemID = fliper.item{
FIRDatabase.database().reference().child("Flip").child("..I need to find out how to get child auto id").removeValue(completionBlock: { (error, ref) in
if error != nil {
print("Failed! to delete message")
return
}
//one way of updating the table
self.flipList.remove(at: indexPath.row)
self.tableViewFlips.deleteRows(at: [indexPath], with: .automatic)
})
}`
The "key" here is to ensure that when the data is initially loaded from Firebase you keep the node key intact. So for example. say you have a messaging app that has posts. Here's a class that would store the data from Firebase
struct PostStruct {
post_key = ""
post_text = ""
posted_by_uid = ""
}
and then those structs would be used to populate your dataSource array.
Given the above structure:
When your dataSource is initially populated from Firebase, as you read in each post node, create a post struct, populate it with data from Firebase and store it in a dataSource, typically an array.
When you swipe to delete there are about 100 options for handling the actual delete process but here are a couple:
1) Your tableView is backed by an array. If you swipe to delete row 2 (for example), inspect the object (struct) to be deleted, capture the post_key from the stuct, then delete the node from Firebase (since you know the node key) and then query the array for that object key, delete the row in the array and reload your tableView/update the UI.
2) When you swipe to delete a row, inspect the object and obtain the post_key. Then remove the object from Firebase. That would trigger a .childRemoved event (assuming you've attached the corresponding listener). When your app receives that event, determine which object it is (via key or other means) and remove it from the array and reload the tableView.
3) If you swipe to delete row 2, get the struct from row 2 in your dataSource array, read the key from that struct, remove row 2, remove the object from Firebase and reload the tableView.
There are many, many other way to tackle this; your dataSource array could store the actual snapshot data from firebase or it could store a series of key: value pairs with the key being the node key and the value being the data within that node (as a stuct, class, string etc).
Edit:
Some additional info to clarify the above.
Supposed we are using the structure above and firebase has matching nodes like this
root
posts
post_key_0 //created via childByAutoId
post_text: "some post text"
posted_by_uid: "uid_0"
post_key_1 //created via childByAutoId
post_text: "another post"
posted_by_uid: "uid_1"
then to read the posts and populate a dataSource array:
struct PostStruct {
post_key = ""
post_text = ""
posted_by_uid = ""
}
var dataSourceArray = [PostStruct]()
func button0() {
let itemsRef = self.ref.child("items")
itemsRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let dict = snap.value as! [String: Any]
let postKey = snap.key
let postText = dict["post_text"] as! String
let postedByUid = dict["posted_by_uid"] as! String
let post = PostStruct(post_key: postKey, post_text: postText, posted_by_uid: ppostedByUid)
self.dataSourceArray.append(post)
}
self.someTableView.reloadData()
})
}
from there it's pretty straightforward.
Assuming we are using option 3), when the user swipes to delete row 2 for example you get that post from the array, row 2
let post = self.dataSourceArray[2]
let postKey = post.post_key
now you know the firebase key so it can then be removed
let thisPostRef = fbRef.child("posts").child(postKey)
thisPostRef.remove()
and finally remove it from the array and refresh the tableView
self.dataSourceArray.remove(at: 2)
self.someTableView.reloadData()
The code would be different if you went with one of the other options but the concepts are similar and can be applied to each.

Firebase query observing reshowing data

I have a firebase query that observes data from a posts child.
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
self.postList.append(post)
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
However, when a user posts something, it creates a more than one cell with the data. For instance, if I post "hello", a two new cards show up with the hello on it. However, when I exit the view and recall the fetch posts function, it shows the correct amount of cells. Also, when I delete a post from the database, it adds a new cell as well and creates two copies of it until I reload the view, then it shows the correct data from the database.
I suspect this has something to do with the observe(.value), as it might be getting the posts from the database and each time the database changes it creates a new array. Thus, when I add a new post, it is adding an array for the fact that the post was added and that it now exists in the database, and when I refresh the view it just collects the data directly from the database.
Also, sometimes the correct amount of cells show and other times there's multiple instances of random posts, regardless of whether I have just added them or not.
How can I change my query so that it initially loads all the posts from the database, and when some post is added it only creates one new cell instead of two?
Edit: The logic seeming to occur is that when the function loads, it gets all the posts as it calls the fetchPosts(). Then, when something is added to the database, it calls the fetchPosts() again and adds the new data to the array while getting all the old data. yet again.
One thing I always do when appending snapshots into an array with Firebase is check if it exists first. In your case I would add
if !self.postList.contains(post) {
self.postList.append...
however, to make this work, you have to make an equatable protocol for what I'm guessing is a Post class like so:
extension Post: Equatable { }
func ==(lhs: Post, rhs: Post) -> Bool {
return lhs.uid == rhs.uid
}
You are right in thinking that the .value event type will return the entire array each time there is a change. What you really need is the query.observe(.childAdded) listener. That will fetch individual posts objects rather than the entire array. Call this in your viewDidAppear method.
You may also want to implement the query.observe(.childRemoved) listener as well to detect when posts are removed.
Another way would be to call observeSingleEvent(.value) on the initial load then add a listener query.queryLimited(toLast: 1).observe(.childAdded) to listen for the latest post.

Cannot successfully reload UITableView elements when parsing data from Firebase

I'm making an app where users can buy and sell tickets. Users are able to create a new ticket and it successfully uploads to firebase however a reference to the ticket ID is stored in the user data which references the ticket id in the ticket data. The structure of the database is below:
DATABASE
USERS
TICKETS
TICKETS
TICKET INFO
USER
USER INFO AND TICKET ID OF TICKETS THEY ARE SELLING
My problem is that the first time I load the tickets from the selling tickets it's fine. However when the user adds a new ticket that they are selling, the table view loads everything twice.
override func viewWillAppear(_ animated: Bool) {
self.tickets = []
DataService.ds.REF_USER_CURRENT.child("selling").observe(.value, with: { (snapshot) in //HERE WE REFERNCE OUR SINGELTON CLASS AND OBSERVE CHAMGE TO THE POSTS OBJECT
self.tickets = [] //WE CLEAR THE POSTS ARRAY BEFORE WE START MANIPULATION TO MAKE SURE THAT WE DONT REPEAT CELLS
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
print("ADAM: \(snapshot)")//CHECKING THAT THE OBJECTS EXIST AS AN ARRAY OF DATA SNAPSHOTS
for snap in snapshot {
DataService.ds.REF_TICKETS.child(snap.key).observe(.value, with: { (snapshot) in
if let ticketDict = snapshot.value as? Dictionary<String, AnyObject>{
let ticket = Ticket(ticketID: snap.key, ticketData: ticketDict)
self.self.tickets.append(ticket)
}
self.sell_ticketsTableView.reloadData()
})
}
}
//self.sell_ticketsTableView.reloadData()
self.tickets = []//RELAOD THE DATA
})
}
I'm not quite sure where I have gone wrong.
Please change your code to this. I have added the part where you clear your array
override func viewWillAppear(_ animated: Bool) {
self.removeAll() // This is how you clear your array
DataService.ds.REF_USER_CURRENT.child("selling").observe(.value, with: { (snapshot) in //HERE WE REFERNCE OUR SINGELTON CLASS AND OBSERVE CHAMGE TO THE POSTS OBJECT
self.tickets = [] //WE CLEAR THE POSTS ARRAY BEFORE WE START MANIPULATION TO MAKE SURE THAT WE DONT REPEAT CELLS
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
print("ADAM: \(snapshot)")//CHECKING THAT THE OBJECTS EXIST AS AN ARRAY OF DATA SNAPSHOTS
for snap in snapshot {
DataService.ds.REF_TICKETS.child(snap.key).observe(.value, with: { (snapshot) in
if let ticketDict = snapshot.value as? Dictionary<String, AnyObject>{
let ticket = Ticket(ticketID: snap.key, ticketData: ticketDict)
self.tickets.append(ticket)
}
self.sell_ticketsTableView.reloadData()
})
}
}
//self.sell_ticketsTableView.reloadData()
})
}
You are observing the value of what a user is selling, which means every time they add something to this list, your listener will trigger and give you the new value of users/$uid/selling in its entirety.
This is why you are seeing double when the user adds another ticket; the listener is triggered and you append each ticket to the array again. You can get around this by checking if the ticket is already in the array before you append it however, your current implementation can be improved.
Instead of using observe(.value, you should use .childAdded. The listener will trigger every time a new child is added and only give you that specific child snapshot.
The listener will initially trigger for each child at that reference and you can append them to the array individually. However, once all the children have been loaded, the next child to be added will trigger this listener, which you can append to the array.

Add value instead of change value In Firebase with Swift

I would like to save and retrieve features to and from Firebase into a TableView.
The child I would like to save them under is the uid (unique user id)
so a feature would look like this in the database:
Firebase database
The ideal situation, is how the "derde" is saved, so the uid as a key and "derde" as the value.
#IBAction func saveButtonPressed(sender: AnyObject) {
let featureContents = addFeatureTextField.text
if featureContents != "" {
// Build the new Feature.
let newFeature: String = featureContents!
let ref = DataService.dataService.FEATURE_REF.childByAppendingPath(uid)
ref.setValue(newFeature)
where uid is a String, retrieved from authdata somewhere else in the code.
If I save it like this, it saves it to the specific uid path. If I want to add another feature by clicking on the + in the TableViewController, it saves it to the same path, so the Firebase database is updated with the new value and so instead of two features you only end up with one updated feature.
You can prevent this by working with the chilByAutoId() method, to save a list of items. The code would look like this:
#IBAction func saveButtonPressed(sender: AnyObject) {
let featureContents = addFeatureTextField.text
if featureContents != "" {
// Build the new Feature.
let newFeature: String = featureContents!
let ref = DataService.dataService.FEATURE_REF.childByAutoId().childByAppendingPath(uid)
ref.setValue(newFeature)
via this way, a feature is saved, as you can see in the above image at: "vierde"
This allows you to save multiple features with all the same uid, but different autoId.
But, if I save it like this, my tableView stays empty. The TableViewController is like this:
DataService.dataService.FEATURE_REF.observeEventType(.Value, withBlock: { snapshot in
// The snapshot is a current look at our features data.
print("The features in the tableView should be \(snapshot.value)")
self.features = []
if let snapshots = snapshot.children.allObjects as? [FDataSnapshot] {
for snap in snapshots {
// Make our features array for the tableView.
if let postDictionary = snap.value as? String {
print("All in")
let key = snap.key
let feature = Feature(key: key, value: postDictionary)
// Items are returned chronologically, but it's more fun with the newest features first.
self.features.insert(feature, atIndex: 0)
}
}
}
// Be sure that the tableView updates when there is new data.
self.tableView.reloadData()
})
}
Problem lies in this code: if let postDictionary = snap.value as? String {
This conditional binding does not succeed, because the value is not a String, but the autoId key has no value, only the child under it which is the uid has a value "vierde"
Two possible solutions which I am asking you guys:
1) How can I save multiple features with the same uid without using the autoId?
2) If I am obliged to use the autoId, how can I make sure it observes the value of the uid key under the autoId, instead of the non existing value of the autoId.
Thanks for your help!
I think the answer to the question is to build a dictionary out of the key:value pairs of data and store that as a child of your uid node
let featureDict = [ "feature_0": "cool feature", "feature_1": "great feature"]
let ref = DataService.dataService.FEATURE_REF.childByAppendingPath(uid)
ref.setValue(featureDict)
results in
the_uid
feature_0: "cool feature"
feature_1: "great feature"
The limitation here is the key's names, and then the ability to add even more data about each feature.
Here's a potentially better option
the_uid
auto_id_0
feature_name: #"cool feature"
summary: "Everything you'd ever want to know about this feature"
auto_id_1
feature_name: #"great feature"
summary: "Info about this great feature"
The auto_id_x is generated by autoId and allows you to add however many features you want, change their names and summaries. etc. The children of each auto_id_x are (or could be) stored in a dictionary and saved per the above example.

Resources