Firebase Sorting - ios

Basically, I have an app on Firebase. The thing is, when Firebase sorts the data, instead of a chronological order, it muddles the data.
When I went online and search why, I found that it was because I was using the snapshot.value instead of snapshot.children.
However, I'm not completely sure how to change the code accordingly, could someone help?
Here is the code:
func retrieveChatLog() {
FIRDatabase.database().reference().child("chats").child(chatID).observe(.value, with: {(snapshot) in
let chats = snapshot.value as! [String : AnyObject]
self.messages.removeAll()
for (_, value) in chats {
if let sender = value["sender"], let message = value["message"], let senderID = value["senderUID"], let date = value["date"] {
let messageToShow = Message()
messageToShow.message = message as! String
messageToShow.sender = sender as! String
messageToShow.senderUID = senderID as! String
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let curDate = formatter.date(from: date as! String)
messageToShow.date = curDate as! Date
if messageToShow.senderUID != "" {
self.messages.append(messageToShow)
}
}
}
self.tableView.reloadData()
})
FIRDatabase.database().reference().removeAllObservers()
}

The problem isn't because you use snapshot.value. You can use .value or .childAdded. The problem is, if you use .value, you don't want to cast the snapshots to a dictionary because dictionaries don't preserve order. Instead, you're going to want to cast to an array to preserve order. Here's one way you could resolve this:
FIRDatabase.database().reference().child("chats").child(chatID).observe(.value, with: {(snapshot) in
let chats = snapshot.children.allObjects as! [FIRDataSnapshot]
self.messages.removeAll()
for chat in chats {
if let value = chat.value as? [String: AnyObject] {
if let sender = value["sender"], let message = value["message"], let senderID = value["senderUID"], let date = value["date"] {
let messageToShow = Message()
messageToShow.message = message as! String
messageToShow.sender = sender as! String
messageToShow.senderUID = senderID as! String
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let curDate = formatter.date(from: date as! String)
messageToShow.date = curDate as! Date
if messageToShow.senderUID != "" {
self.messages.append(messageToShow)
}
}
}
}
self.tableView.reloadData()
})
FIRDatabase.database().reference().removeAllObservers()
}

Change this line
FIRDatabase.database().reference().child("chats").child(chatID).observe(.value, with: {(snapshot) in...
To this
FIRDatabase.database().reference().child("chats").child(chatID).observe(.childAdded, with: {(snapshot) in

There is an accepted answer but I wanted to provide another solution that may be a bit tighter and more expandable.
It starts with a class to hold the messages. In this case I am keeping just the firebase key and timestamp but other vars could easily be added. You'll note the timestamp var that is read from Firebase in the same format: dd.mm.yy. This would be useful for sorting if needed. Also note that if you want to display a nicely formatted mm/dd/yyyy format, it's available though the formattedDate computed property.
The messagesArray is an array of MessageClass objects which can be used as a datasource for a tableView for example.
Finally, the loadMessages function to load in all of the messages. As mentioned in the other answer, casting the Snapshot to a dictionary loses ordering guarantee. However, if we iterate over the snapshot directly, the ordering stays intact.
class MessageClass {
var timestamp: String
var fbKey: String
var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let d = formatter.date(from: timestamp)
let outputFormattter = DateFormatter()
outputFormattter.dateFormat = "MM/dd/yyyy"
let finalDate = outputFormattter.string(from: d!)
return finalDate
}
init(withSnap: DataSnapshot ) {
let snapDict = withSnap.value as! [String: AnyObject]
self.timestamp = snapDict["timestamp"] as! String
self.fbKey = withSnap.key
}
}
var messagesArray = [MessageClass]()
func doButton0Action() {
let messagesRef = self.ref.child("messages")
messagesRef.observe(.value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let m = MessageClass(withSnap: snap)
self.messagesArray.append(m)
}
for msg in self.messagesArray { //test code to print the array once loaded
print("key: \(msg.fbKey) date: \(msg.formattedDate)")
}
})
}
This code is very verbose and could be condensed considerably but it's very readable. (it needs guards and error checking as well).
I would strongly encourage you to store your timestamps in a
yyyymmddhhmmss
format in firebase. It lends itself to sorting / querying
Also, as a side-note, instead of relying on the date the node was created (by key) to keep your ordering, consider leveraging the timestamp when reading in the nodes using .order(by: timestamp). That will guarantee they are always in the correct order even if messages are changed around or the keys are modified.

Related

Sort objects by date/Firestore

I would like to sort my data by date from newest down to oldest. I am getting Transaction objects from Firestore in the form of "2020-12-29". I have seen previous answers on how to sort arrays by date but I am unsure how I can sort it with my current object structure.
func loadTransactions(){
if let catId = self.categoryId{
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
self.budgetData.removeAll()
for document in snapshot!.documents {
let data = document.data()
let title = data["name"] as? String ?? ""
let date = data["date"] as? String ?? ""
let amount = data["amount"] as? Double ?? 0
let id = data["transId"] as? String ?? ""
let absolute = abs(amount)
let trans = Transaction(transId: id, catId:catId,title: title, dateInfo: date, image: UIImage.grocIcon, amount: absolute)
self.budgetData.append(trans)
// let testArray = ["25 Jun, 2016", "30 Jun, 2016", "28 Jun, 2016", //"2 Jul, 2016"]
// var convertedArray: [Date] = []
// var dateFormatter = DateFormatter()
// dateFormatter.dateFormat = "dd MM, yyyy"// yyyy-MM-dd"
// for dat in testArray {
// let date = dateFormatter.date(from: dat)
// if let date = date {
// convertedArray.append(date)
// }
// }
// var ready = convertedArray.sorted(by: { $0.compare($1) == .orderedDescending })
var tranSet = Set<Transaction>()
self.budgetData = self.budgetData.filter { (transaction) -> Bool in
if !tranSet.contains(transaction){
tranSet.insert(transaction)
return true
}
return false
}
DispatchQueue.main.async {
self.setTotalAmountOfCats()
self.tableView.reloadData()
}
}
}
}
}
}
You can sort the data in your Firebase query using order(by:): https://firebase.google.com/docs/firestore/query-data/order-limit-data
In your example, it'll probably look like:
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.order(by: "date")
If you implemented this, you could get rid of your tranSet and just append the Transactions in order (this is out of scope of the question, but I'd probably look at compactMap to transform snapshot.documents instead of appending them in order).
This assumes that your dates are in the format you listed (YYYY-MM-DD), which will alphabetize correctly, even though they aren't actually date/timestamp objects. If your data is in the format in your commented-out testArray section, than a different method would have to be used.

How to save tableView separation after restart when connected to Firebase

I am new to programming and currently trying things with firebase. I am building a newsfeed like app, which displays custom type cells in my tableView. I currently have the problem that after an app restart the separation is like the one in firebase. But I want the newest be first and stay there. I thought about saving the array in core data, but I don't like this option as much. So my question is: how do I sort the posts from newest to oldest?
var ref: DatabaseReference!
var posts = [String]()
var timeRef = [String]()
var userRef = [String]()
override func viewDidLoad() {
super.viewDidLoad()
addSlideMenuButton()
ref = Database.database().reference()
ref.child("posts").observe( .childAdded, with: { snapshot in
let newPost = snapshot.value as? NSDictionary
let post_title:String = newPost!["post_title"] as? String ?? "error"
let newPostTime = snapshot.value as? NSDictionary
let post_time:String = newPostTime!["post_time"] as? String ?? "error"
let newPostUser = snapshot.value as? NSDictionary
let post_user:String = newPostUser!["post_user"] as? String ?? "Team"
self.posts.insert(post_title, at: 0)
self.timeRef.insert(post_time, at: 0)
self.userRef.insert(post_user, at: 0)
self.newsfeedTableView.reloadData()
})
}
(Tell me if you need more code)
I'm not familiar with firebase but it might have something built in that might help you with that task as it's a common one.
Anyway, this is how you may do it with your method:
let isoFormatter = ISO8601DateFormatter()
// dateStrings will be fetched from your database
let dateStrings = [
"2018-12-07T09:21:42Z",
"2018-12-07T09:19:42Z",
"2018-12-07T09:20:42Z"
]
// here we're converting the strings to an actual Date types
let dates = dateStrings.compactMap { isoFormatter.date(from: $0) }
// sorting in descending order
let sortedDates = dates.sorted { $0 > $1 }
print(sortedDates)
/*
prints-
[
2018-12-07 09:21:42 +0000,
2018-12-07 09:20:42 +0000,
2018-12-07 09:19:42 +0000
]
*/
You can use isoFormatter.string(from: Date) to upload a date to your database

Return more concrete objects from Data Model in Swift

I am working on an app which fetches data from Firebase. The code I am going to show you works just fine but even as a rookie I know that the implementation should be waaaaay better. I just can't really figure out how to refactor this.
Here's the TLDR:
I have created a Data Model which I used for pasing BlogPosts. I use a Struct for this and in the initial version all of my properties were of type String.
However, apart from Strings for Title and Summary, my Posts also contain an URL to an image and also a Date (post date).
I want to be able to return from my BlogPost object more concrete objcets such as an already created URL or a Date object.
Like I said, I know my implementation is bad, and I want to learn a better way of typecasting in such a way that I can achieve the behaviour described above.
Here is the implementation of my BlogPost data model:
import Foundation
import FirebaseDatabase
struct BlogPost {
let key: String
let title: String
let body: String
let summary: String
let author: String?
let liveSince: Date
let featuredImageLink: URL?
let itemReference:DatabaseReference?
init(snapshot: DataSnapshot) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-mm-dd"
key = snapshot.key
itemReference = snapshot.ref
if let snapshotDictionary = snapshot.value as? NSDictionary, let postTitle = snapshotDictionary["Title"] as? String {
title = postTitle
} else {
title = "Cannot display Title for this item :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postBody = snapshotDictionary["Body"] as? String {
body = postBody
} else {
body = "Cannot display Body for this item :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postSummary = snapshotDictionary["Summary"] as? String {
summary = postSummary
} else {
summary = "Due to some weird error, the Summary for this item cannot be displayed. Insert more coffee and Pizza in developer"
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postAuthor = snapshotDictionary["Author"] as? String {
author = postAuthor
} else {
author = "Nobody wrote this :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postImageLink = snapshotDictionary["FeaturedImage"] as? String {
featuredImageLink = URL(string: postImageLink)!
} else {
featuredImageLink = URL(string:"https://someimagelink")!
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let liveDate = snapshotDictionary["LiveSince"] as? String {
if let live = dateFormatter.date(from: liveDate) {
liveSince = live
} else {
liveSince = dateFormatter.date(from: "1990-06-26")!
}
} else {
liveSince = dateFormatter.date(from: "1990-06-26")!
}
}
}
Any constructive feedback is more than welcome as I do really want to understand how to do this properly or if it even makes sense to do so in the first place!
Thank you very much for your replies in advance!
I have some suggestions. It looks you keep using conditional binding to unwrap the snapshsot.value while casting it as an NSDictionary. Since unwrapping this value is prerequisite to unwrapping the other values, why not just use a guard statement to unwrap it? In your guard statement, you can then default initialize all the properties in your struct. Alternatively, if you are not adamant about default initializing your properties, you can just use a failable initializer and just return nil in the guard statement.
guard let snapshotDictionary = snapshot.value as? NSDictionary else {
title = "Cannot display Title for this item :("
body = "Cannot display Body for this item :("
summary = "Due to some weird error, the Summary for this item cannot be
displayed. Insert more coffee and Pizza in developer"
...
...
}

Update Firebase data in tableView cell on tap

I am using Firebase real time database to display posts in a tableView. I want to increase the number of likes of a specific post when the user double taps the corresponding cell.
I got the double tap working and am already printing out the correct indexPath.
override func viewDidLoad() {
super.viewDidLoad()
// double tap
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(sender:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
postTableView.addGestureRecognizer(doubleTapGestureRecognizer)
}
And here's what I tried according to the Firebase documentation to update the likes:
func handleDoubleTap(sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: postTableView)
if let indexPath = postTableView.indexPathForRow(at: touchPoint) {
print(indexPath)
let post = posts[indexPath.row]
let oldLikes = post.likes
let newLikes = oldLikes! + 1
let postUpdates = ["\(post.likes)": newLikes]
database.updateChildValues(postUpdates)
postTableView.reloadData()
}
}
It doesn't throw any errors but is not working.
This is the database structure:
And here's how I declared the database:
struct post {
let author : String!
let creationDateTime : String!
let content : String!
let likes : Int!
}
And in viewDidLoad
let database = FIRDatabase.database().reference()
This is how I create a post:
#IBAction func savePost(_ segue:UIStoryboardSegue) {
let addPostVC = segue.source as! AddPostViewController
let author = currentUser.displayName
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let dateResult = formatter.string(from: date)
let creationDateTime = "\(dateResult)"
let content = addPostVC.passTextContent
let likes = 0
let post : [String : AnyObject] = ["author" : author as AnyObject,
"content" : content as AnyObject,
"creationDateTime" : creationDateTime as AnyObject,
"likes" : likes as AnyObject]
database.child("Posts").childByAutoId().setValue(post)
}
And this how I retrieve the data in viewDidLoad
database.child("Posts").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let postID = (snapshot.value as? NSDictionary)?["postID"] as? String ?? ""
let author = (snapshot.value as? NSDictionary)?["author"] as? String ?? ""
let content = (snapshot.value as? NSDictionary)?["content"] as? String ?? ""
let creationDateTime = (snapshot.value as? NSDictionary)?["creationDateTime"] as? String ?? ""
let likes = (snapshot.value as? NSDictionary)?["likes"] as? Int ?? 0
self.posts.insert(post(postID: postID, author: author, creationDateTime: creationDateTime, content: content, likes: likes), at: 0)
self.postTableView.reloadData()
})
You have a problem with your database reference. When you do let database = FIRDatabase.database().reference() then you are refering to the main node in your database structure. This means that you will be working with the structure under the root in your database Json. The only child of it will be the Posts key.
When you do
let postUpdates = ["\(post.likes)": newLikes]
database.updateChildValues(postUpdates)
you are trying to update the value under the root node, which clearly does not exist. The only reference it can find is the key Posts.
In order to perform the update in the correct place, you could get child references from your main reference, especially one to the post you are interested in updating.
For example, you could do the following:
let postReference = database.child("Here goes the post Id").
Then, you will be able to use updateChildValues correctly on this new reference, since it will be updating the specific post.
Another thing that may be used wrong is the dictionary that is being sent to the updateChildValues. The structure of the dictionary that you have to provide is the following:
["key that you want to update": new value]
So in your case, instead of providing the previous like count and the new like count, you should provide a dictionary as the following:
let postUpdates = ["likes": newLikes]

How to ensure that the data is not retrieved and appended as a whole each time a new entry is added?

func generateDataForRecents() {
if URLArrayStringThisSeason.count == 0 {
self.activityIndicator2.isHidden = false
self.activityIndicator2.startAnimating()
}
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("palettes").queryLimited(toFirst: 100).observe(.value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict as [String:AnyObject]{
let URL = each.value["URL"] as! String
self.URLArrayStringRecents.append(URL)
//print(self.URLArrayString.count)
//print(snapshot)
//let pictureTitle = each.value["title"] as! String
print(self.URLArrayStringRecents.count)
}
}
self.whatsNewCollectionView?.reloadData() //Reloads data after the number and all the URLs are fetched
self.activityIndicator2.stopAnimating()
self.activityIndicator2.isHidden = true
})
}
The following code does a retrieval of data each time the function is called, or when a new data is added.
This is extremely useful when the app is first started up or closed and then restarted. However, when the app is running, whenever a new entry is added, the code seemed to run again and thus appending twice the amount of new data.
For example, when there are already 15 entries identified and then suddenly a new entry is added, the array of the URL would contain 15+16 thus amounting to a total of 31.
How do I make it such that the new data is added to the array instead of adding the entire snapshot in?
You do that by listening for .childAdded events, instead of listening for .value:
var query = databaseRef.child("palettes").queryLimited(toFirst: 100)
query.observe(.childAdded, with: { (snapshot) in
let URL = snapshot.childSnapshot(forPath/: "URL").value as! String
self.URLArrayStringRecents.append(URL)
}
Since you have a limit-query, adding a 101st item means that one item will be removed from the view. So you'll want to handle .childRemoved too:
query.observe(.childRemoved, with: { (snapshot) in
// TODO: remove the item from snapshot.key from the araay
})
I recommend that you spend some time in the relevant documentation on handling child events before continuing.
Please check below method. I have use this method not getting any duplicate entry.
func getallNotes()
{
let firebaseNotesString: String = Firebase_notes.URL
let firebaseNotes = FIRDatabase.database().referenceFromURL(firebaseNotesString).child(email)
firebaseNotes.observeEventType(.Value, withBlock: { snapshot in
if snapshot.childSnapshotForPath("Category").hasChildren()
{
let child = snapshot.children
self.arrNotes = NSMutableArray()
self.arrDictKeys = NSMutableArray()
for itemsz in child
{
let childz = itemsz as! FIRDataSnapshot
let AcqChildKey : String = childz.key
if AcqChildKey == AcqIdGlobal
{
if (childz.hasChildren() == true)
{
let dictChild = childz.value as! NSMutableDictionary
self.arrDictKeys = NSMutableArray(array: dictChild.allKeys)
for i in 0..<self.arrDictKeys.count
{
let _key = self.arrDictKeys.objectAtIndex(i).description()
print(_key)
let dictData : NSMutableDictionary = NSMutableDictionary(dictionary: (dictChild.valueForKey(_key)?.mutableCopy())! as! [NSObject : AnyObject])
dictData.setObject(_key, forKey: "notesId")
self.arrNotes.addObject(dictData)
}
}
}
}
self.tableviewNote.reloadData()
}
})
}
As for the query for removed child,
query.observe(.childRemoved, with: { (snapshot) in
print(snapshot)
let URL = snapshot.childSnapshot(forPath: "URL").value as! String
self.URLArrayStringThisSeason = self.URLArrayStringThisSeason.filter() {$0 != URL}
self.thisSeasonCollectionView.reloadData()
})
it will obtain the URL of the removed child and then update the array accordingly.

Resources