I'm trying to order my data from Firebase so the most recent post is at the top (like Instagram), but I just can't get it to work properly. Should I be using a server timestamp? Is there a "createdAt" field?
func getPosts() {
POST_REF.observeEventType(.Value, withBlock: { snapshot in
guard let posts = snapshot.value as? [String : [String : String]] else {
print("No Posts Found")
return
}
Post.feed?.removeAll()
for (postID, post) in posts {
let newPost = Post.initWithPostID(postID, postDict: post)!
Post.feed?.append(newPost)
}
Post.feed? = (Post.feed?.reverse())!
self.tableView.reloadData()
}, withCancelBlock: { error in
print(error.localizedDescription)
})
}
Using only reverse() for your array is not enough way to encompass everything. There are different things you need to think about:
Limit while retrieving data, use append() and then reverse() to save time. You don't need to delete all array for each time.
Scroll trigger or willDisplay cell method loading
Let's start. You can create a child for your posts timestamp or date/time being global. To provide like Instagram seconds, weeks I advice you using UTC time. So I will call this: (timeUTC)
For sorting your all post, use since1970 timestamp. So I will call this (timestamp) and then also you can keep another node as (reversedTimestamp) adding - prefix to timestamp. So when you use queryOrdered to this node. You can handle latest 5 post using with yourQuery.queryLimited(toFirst: 5).
1.Get UTC date/time for timeUTC node in Swift 3:
let date = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"
formatter.timeZone = TimeZone(abbreviation: "UTC")
let utcTimeZoneStr = formatter.string(from: date)
+0000 means it's universal time, look at http://time.is/tr/UTC
2.Get since1970 timestamp to sort posts in Swift 3:
let timestamp = (Date().timeIntervalSince1970 as NSString).doubleValue
let reversedTimestamp = -1.0 * timestamp
Now, you can save them on your Firebase posts like this.
"posts" : {
"-KHLOy5mOSq0SeB7GBXv" : {
"timestamp": "1475858019.2306"
"timeUTC" : "2012-02-04 12:11:56 +0000"
},
"-KHLrapS0wbjqPP5ZSUY" : {
"timestamp": "1475858010.1245"
"timeUTC" : "2014-02-04 12:11:56 +0000"
},
I will retrieve five by five post, so I'm doing queryLimited(toFirst: 5) in viewDidLoad:
let yourQuery = ...queryOrdered(byChild: "reverseTimestamp")
.queryEnding(atValue: "\(self.pageOnTimestamp)", childKey: "reverseTimestamp")
yourQuery.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.value is NSNull {
print("There is no post.")
}
else {
yourQuery.queryLimited(toFirst: 5).observeSingleEvent(of: .value, with: { (snapshot) in
self.posts.removeAll(keepingCapacity: true)
for (i, snap) in snapshot.children.enumerated() {
if let postAllDict = snapshot.value as? [String: AnyObject] {
if let postDict = postAllDict[(snap as AnyObject).key as String] as? [String: AnyObject] {
let post = Post(key: (snap as AnyObject).key as String, postDict: postDict)
self.posts.append(post)
}
}
}
completion(true)
})
}
})
If user reached latest post, you can handle it with willDisplay method like below, then you can call loadMore function.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.posts.count - 1 == indexPath.row {
// call loadMore function.
}
}
In loadMore() function you can handle latest post's timestamp, then start-end query as with that, so you can easily continue with next first 5 posts while appending before array.
For Swift 3 conversion as nice formatted, take a look here: Swift 3 - UTC to time ago label, thinking 12h / 24h device time changes
Based on #tobeiosdev answer i define my data structure like this:
"posts" : {
"-KcOa8GXBl08V-WXeX8P" : {
"author" : "#giovanny.piƱeros",
"hashtags" : [ "Hello", "World" ],
"text" : "Hola Mundo!!!!!",
"timestamp" : 5.08180914309278E8,
"title" : "Hello World",
"userid" : "hmIWbmQhfgh93"
}
As you can see i've added a timestamp attribute, when i query my data with a child added event, i pass that data to a post object, then i append that object to an array of posts from which my table view will feed:
self.posts.append(post)
self.posts.sort(by: {$0.timestamp! > $1.timestamp!})
self.tableViewFeed.reloadData()
With the sort method i've managed to order my data in the desire order of posts, from the newest to the oldest. I Hope this approach could help anyone :).
Related
I am building a calendar app and I have developed it so that when you click on a date it will show the event corresponding to that date. I have it so that the date will appear in the tableview and when you click on the event it will then segue to a detail view controller and show the details of that event. I am stuck because I am not sure how to get firebase to load multiple events onto one date or how to show multiple events with the same date.
My Code:
View Did Load:
override func viewDidLoad() {
super.viewDidLoad()
calendar.dataSource = self
calendar.delegate = self
self.calendar.calendarHeaderView.backgroundColor = UIColor.blue
navigationController?.navigationBar.barTintColor = UIColor.blue
calendar.appearance.titleWeekendColor = UIColor.red
retrieveEventsFromDatabase()
print(eventsArray)
}
How I am getting the Data:
func retrieveEventsFromDatabase() {
dbReference = Database.database().reference().child("calendarevents")
dbReference.observe(DataEventType.value, with: { [weak self] (snapshot) in
guard snapshot.childrenCount > 0 else { return }
var events: [EventsDataModel] = []
for event in snapshot.children.allObjects as! [DataSnapshot]
{
let object = event.value as? [String: AnyObject]
let eventName = object?["eventName"]
let eventDate = object?["eventDate"]
let eventColor = object?["eventColor"]
let event = EventsDataModel(eventName: eventName as! String, eventDate: eventDate as! String,eventColor: eventColor as! String)
events.append(event)
self?.eventsArray = events
//print(events)
}
DispatchQueue.main.async {
self?.eventsTable.reloadData()
self?.calendar.reloadData()
}
})
}
How i am showing it in my table and calendar:
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = self.dateFormatter2.string(from: date)
print("selected dateString = \(dateString)")
tableArray.removeAll()
for event in eventsArray {
if event.eventDate.contains(dateString) {
selectedDateLabel.text = ("The selected date is \(dateString)")
print("The event for this date is \(event.eventName)")
tableArray.append(event)
break;
} else {
selectedDateLabel.text = "select a date to see the events for this day"
print("=select a date to see the events for this day")
}
}
self.eventsTable.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let friend = tableArray[indexPath.row].eventName
let cell = eventsTable.dequeueReusableCell(withIdentifier: "eventCell", for: indexPath) as! EventsTitleTableViewCell
cell.eventTitleLabel?.text = friend
return cell
}
Firebase Database:
{
"2021-01-12" : {
"eventColor" : "red",
"eventDate" : "2021-01-12",
"eventName" : "SignUp Day"
},
"2021-01-15" : {
"eventColor" : "red",
"eventDate" : "2021-01-15",
"eventName" : "First Day"
},
"electionday" : {
"eventColor" : "red",
"eventDate" : "2021-02-20",
"eventName" : "Election Day "
},
"groundhogday" : {
"eventColor" : "red",
"eventDate" : "2021-02-20",
"eventName" : "GroundHog Day"
}
}
Picture of my App for more details:
I think that a better and more logical approach would be to store the event, rather than dates, and then every event has a corresponding date.
When the user taps on the day, you fetch all the events that have the date property equal to that date.
So for anybody that is looking at this in the future. I want to answer my own question. For any references I was using this with FSCalendar and Firebase. The answer to my question was that there was a "break;" as seen after the tableArray(append). That was only allowing one of my events to show on the table. When i commented that out, then both the events would show on my table. Thanks!
I am working on weight tracking app when i am adding new data to firebase it is work properly but when i am updating the data from that date the new data is adding with existing emails unique id i want to update current dates weight if i add weight again on that date
[the black round showing where i want to update the weight and remaining other data is where actual data is updating
here is my code too.
#IBAction func addButton(_ sender: UIButton) {
let time = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
let formatteddate = formatter.string(from: time as Date)
if formatteddate == addDate.text!{
self.UpdateTaskWithName(name: addWeight.text!, date: addDate.text!) { (success) in
print("**** TASK UPDATED *******")
DispatchQueue.main.async {
self.navigationController?.popViewController(animated: true)
}
}
}else{
if let text = self.addWeight.text{
if let date = self.addDate.text{
self.saveTaskWithName(name: text, date: date, completionHandler: { (success) in
print("**** NEW TASK ADDED *******")
DispatchQueue.main.async {
self.navigationController?.popViewController(animated: true)
}
})
}
}
}
}
func saveTaskWithName(name:String,date:String, completionHandler:#escaping ((_ success:Bool)->Void)){
let dict = ["weight": name, "date" : date] as [String : Any]
if let userid = Auth.auth().currentUser?.uid{
self.ref.child("WeightTracker").child(userid).childByAutoId().setValue(dict) { (error, reference) in
if let err = error{
print(err.localizedDescription)
}else{
// data save successfully
completionHandler(true)
}
}
}
}
func UpdateTaskWithName(name:String,date:String, completionHandler:#escaping ((_ success:Bool)->Void)){
let dict = ["weight": name, "date" : date] as [String : Any]
if let userid = Auth.auth().currentUser?.uid{
self.ref.child("WeightTracker").child(userid).updateChildValues(dict) { (error, reference) in
if let err = error{
print(err.localizedDescription)
}else{
// data save successfully
completionHandler(true)
}
}
}
}
With this example you can do a query to find the node where your date matches the one you are interested to update if found. That query will return you the key to the node (for the childByAutoId node) that you can use to update the child weight:
func UpdateTaskWithName(name:String, date:String, completionHandler:#escaping ((_ success:Bool)->Void)){
if let userid = Auth.auth().currentUser?.uid {
self.ref.child("WeightTracker")
.child(userid)
.queryOrdered(byChild: "date")
.queryEqual(toValue: date)
.observeSingleEvent(of: .value, with: { snapshot in
self.ref.child("WeightTracker").child(userid).child(snapshot.key).child("weight").setValue(name)
})
}
}
This would be how your function would look like taking in consideration that with name you mean your date coming from the UI TextField and considering.
The function could be also made more meaningful to read by splitting some of the actions into variable instances and re-using them like this:
func UpdateTaskWithName(name:String, date:String, completionHandler:#escaping ((_ success:Bool)->Void)){
if let userID = Auth.auth().currentUser?.uid {
let userIDRef = self.ref.child("WeightTracker").child(userID)
let query = userIDRef.queryOrdered(byChild: "date").queryEqual(toValue: date)
query.observeSingleEvent(of: .value, with: { snapshot in
let childByAutoIDNodeKey = snapshot.key
let refToUpdate = userIDRef.child(childByAutoIDNodeKey).child("weight")
refToUpdate.setValue(name)
completionHandler(true)
})
}
}
To find a solution, let's first identify the issue.
Here's your structure
Weighttracker
user_uid_0
child_by_auto_id_0
date: "19.05.2019"
weight: "236"
assuming you want to update the weight for that date, here's your update code
self.ref.child("WeightTracker").child(userid).updateChildValues(dict)
Remember that when using updateChildValues, if they don't exist, they will be added. Here's the path you're code updating those child values at
WeightTracker/uid/values being updated here
when what you want is
WeighTtracker/uid/childByAutoId/values being updated here
^^^^^^^^^^^^^
The problem is that you don't know what childByAutoId is so you cannot directly update that data.
There are a couple of of solutions; one being to use the date as the key to the node, which would mean your structure would need to be changed
WeightTracker
user_uid_0
20190519
weight: "236"
Then the update becomes simple
let userid = Auth.auth().currentUser.uid
let pathRef = self.ref.child(WeightTracker).child(userid).child(date)
pathRef.child("weight").setValue(weight)
Another option using your existing structure is to query for the node where date equals the date you want, then get the key to that node, then do the update to the child weight node
func updateViaQuery() {
let uid = //users uid
let uidRef = self.ref.child("WeightTracker").child(uid)
let query = uidRef.queryOrdered(byChild: "date").queryEqual(toValue: "19.05.2019")
query.observeSingleEvent(of: .value, with: { snapshot in
let key = snapshot.key
let refToUpdate = uidRef.child(key).child("weight")
refToUpdate.setValue(weight)
})
}
The only issue with the second method is that the query would return no results if the date doesn't exist so you would want to ensure it does before doing an update.
I need to arrange my firebase data by date (unix). I thought queryOrdered(byChild: "date") would do the trick. Done a search and found this which makes sense:
But when you request the .value of the snapshot, the keys+data are converted to a Dictionary. Since a dictionary does not have an extra place to put the information about the order, that information is lost when converting to a dictionary.
Using the same json but modified with unix dates...:
{
"users" : {
"alovelace" : {
"name" : "Last Year",
"date" : 1480550400
},
"eclarke" : {
"name" : "New Year Now",
"date" : 1483228800
},
"ghopper" : {
"name" : "New Year",
"date" : 1483228800
}
}
}
... how to sort when my code is like this:
DataService.ds.REF_INCOMES.queryOrdered(byChild: "date").observe(.value, with: { (snapshot) in
if let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] {
print(snapshot)
for snap in snapshot {
if let incomeDict = snap.value as? [String: AnyObject] { // What needs to change here?
let key = snap.key
let income = Income(incomeId: key, incomeData: incomeDict)
self.incomes.append(income)
self.incomes.reverse()
}
}
}
self.tableView.reloadData()
})
The image below, "Last Year" should be last but it's not:
I have a ruby mentality so Im lost with swift. Thanks.
I think what you're missing here is the sort function, Please try the code below and let me know what are the results:
DataService.ds.REF_INCOMES.queryOrdered(byChild: "date").observe(.value, with: { (snapshot) in
guard let usersSnapshot = snapshot.value as? [String:NSObject] else{
return
}
let users = usersSnapshot["users"] as! [String:AnyObject]
let sorted = users.sorted{($0.0.value["date"] as! Double) > ($0.1.value["date"] as! Double)}
print(sorted) // It should be: New Year Now, New Year, Last Year
})
I believe the problem is that you are observing by .value, which essentially ignores the order by. Try to observe by .childAdded, which does respect the order by operation.
For more info, read the first "Pro Tip": https://howtofirebase.com/collection-queries-with-firebase-b95a0193745d
So I have a feed that users will post into and would like each new submission to go to the top of my collection view. I am ordering each new post by descending time using the negative value of the UTC time shown here: How do you properly order data from Firebase chronologically.
"Posts" : {
"-KHLOy5mOSq0SeB7GBXv" : {
"timestamp" : "1476028495915",
"descendStamp": "-1476028495915"
"otherData": ""
},
"-KHLrapS0wbjqPP5ZSUY" : {
"timestamp" : "1476028496102"
"descendStamp" : "-1476028496102"
"otherData": ""
},
However, each new real time post added is being put on the bottom when initially loaded. Believe this is due to the fact that the observer is asynchronously loading only newly added children and not downloading any other information again, which leads it to ignore my
.queryOrderedByChild("descendStamp")
After leaving and returning to the view, it properly organizes the recently added post to the top. Do I need to client side filter in order to get these new posts at the top in real time? Or what would be the best way to reorganize the order of the posts as the newer ones come in?
var posts = [PostModel]()
let postQuery = ref.child("Posts").queryOrderedByChild("descendStamp")
postQuery.observeEventType(.ChildAdded, withBlock: { (snap) in
if snap.exists() {
let postData = snap.value! as! [String: AnyObject]
let post = PostModel()
post.timestamp = postData["timestamp"] as? NSTimeInterval
post.descendStamp = postData["descentStamp"] as? NSTimeInterval
post.otherData = postData["otherData"] as? String
self.posts.append(post)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView!.reloadData()
})
}
Just sort your dataSource before you update your tableView.
self.posts.sort(by: {$0.descendStamp > $1.descendStamp})
Your code would look something like this:-
var posts = [PostModel]()
let postQuery = ref.child("Posts").queryOrderedByChild("descendStamp")
postQuery.observeEventType(.ChildAdded, withBlock: { (snap) in
if snap.exists() {
let postData = snap.value! as! [String: AnyObject]
let post = PostModel()
post.timestamp = postData["timestamp"] as? NSTimeInterval
post.descendStamp = postData["descentStamp"] as? NSTimeInterval
post.otherData = postData["otherData"] as? String
self.posts.append(post)
self.posts.sort(by: {$0.descendStamp > $1.descendStamp})
dispatch_async(dispatch_get_main_queue(), {
self.posts.sort(by: {$0.descendStamp > $1.descendStamp})
self.collectionView!.reloadData()
})
}
I'm trying to use Firebase timestamps in a Swift app. I'd like to store them in my Firebase, and use them as native NSDate objects in my app.
The docs say they are unix epoch time, so I've tried:
NSDate(timeIntervalSince1970:FirebaseServerValue.timestamp)
with no luck.
This:
FirebaseServerValue.timestamp
returns
0x00000001199298a0
according to the debugger. What is the best way to pass these timestamps around?
ServerValue.timestamp() works a little differently than setting normal data in Firebase. It does not actually provide a timestamp. Instead, it provides a value which tells the Firebase server to fill in that node with the time. By using this, your app's timestamps will all come from one source, Firebase, instead of whatever the user's device happens to say.
When you get the value back (from a observer), you'll get the time as milliseconds since the epoch. You'll need to convert it to seconds to create an NSDate. Here's a snippet of code:
let ref = Firebase(url: "<FIREBASE HERE>")
// Tell the server to set the current timestamp at this location.
ref.setValue(ServerValue.timestamp())
// Read the value at the given location. It will now have the time.
ref.observeEventType(.Value, withBlock: {
snap in
if let t = snap.value as? NSTimeInterval {
// Cast the value to an NSTimeInterval
// and divide by 1000 to get seconds.
println(NSDate(timeIntervalSince1970: t/1000))
}
})
You may find that you get two events raised with very close timestamps. This is because the SDK will take a best "guess" at the timestamp before it hears back from Firebase. Once it hears the actual value from Firebase, it will raise the Value event again.
For me in swift 5 use in another class:
import FirebaseFirestore
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let stamp = data["timeStamp"] as? Timestamp else {
return nil
}
let date = stamp.dateValue()
}
This question is old, but I recently had the same problem so I'll provide an answer.
Here you can see how I am saving a timestamp to Firebase Database
let feed = ["userID": uid,
"pathToImage": url.absoluteString,
"likes": 0,
"author": Auth.auth().currentUser!.displayName!,
"postDescription": self.postText.text ?? "No Description",
"timestamp": [".sv": "timestamp"],
"postID": key] as [String: Any]
let postFeed = ["\(key)" : feed]
ref.child("posts").updateChildValues(postFeed)
The particularly relevant line of code is "timestamp": [".sv": "timestamp"],
This saves the timestamp as a double in your database. This is the time in milliseconds so you need to divide by 1000 in order to get the time in seconds. You can see a sample timestamp in this image.
To convert this double into a Date I wrote the following function:
func convertTimestamp(serverTimestamp: Double) -> String {
let x = serverTimestamp / 1000
let date = NSDate(timeIntervalSince1970: x)
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .medium
return formatter.string(from: date as Date)
}
This gives a timestamp that looks like this:
You will get the right time if you use:
let timestamp = FIRServerValue.timestamp()
let converted = NSDate(timeIntervalSince1970: timestamp / 1000)
let dateFormatter = NSDateFormatter()
dateFormatter.timeZone = NSTimeZone.localTimeZone()
dateFormatter.dateFormat = "hh:mm a"
let time = dateFormatter.stringFromDate(converted)
let serverTimeStamp = ServerValue.timestamp() as! [String:Any]
Store in Firebase something like [ktimeStamp:timestamp as AnyObject]
than after you convert in seconds using Firebase Server Time:
let timestampDate = NSDate(timeIntervalSince1970: Double(timestamp as! NSNumber)/1000)
Firestore has an API for this --> -(NSDate *)dateValue
For example, if you have saved(set) a new document with a field "createdAtDate"
NSDictionary *dataToBeSaved = #{
//Tell the server to save FIRTimestamps when the document is created
#"createdAtDate":[FIRFieldValue fieldValueForServerTimestamp],
#"lastModifiedDate":[FIRFieldValue fieldValueForServerTimestamp],
//Other fields
#"userName":#"Joe Blow"
}
[myFirReference setData:[dataToBeSaved]
options:[FIRSetOptions merge]
completion:^(NSError* error) {
}
You can get back this information either with a get query or via setting a listener. When you have the snapshot back, just access the dates you saved and convert to NSDate.
NSDate *date1 = [snapshot.data[#"createdAtDate"] dateValue];
NSDate *date2 = [snapshot.data[#"lastModifiedDate"] dateValue];
There will be a slight loss in precision, but as most people use dates for data synchronization or sorts, I can't think of a case where the loss of precision would be an issue.
You can create a new transformer for ObjectMapper,
import Foundation
import ObjectMapper
class FirebaseDateTransform: TransformType {
public typealias Object = Date
public typealias JSON = Double
open func transformFromJSON(_ value: Any?) -> Date? {
if let millisecondsSince1970 = value as? Double {
return Date(timeIntervalSince1970: millisecondsSince1970 / 1000.0)
}
return nil
}
open func transformToJSON(_ value: Date?) -> Double? {
if let date = value {
return Double(date.timeIntervalSince1970) * 1000.0
}
return nil
}
}
Gist
Here is some code, based on alicanbatur's answer, that allows a date to be a Double or a server timestamp, and yet still work within an object mapping layer such as ObjectMapper.
enum FirebaseDate {
case date(Date)
case serverTimestamp
var date: Date {
switch self {
case .date(let date):
return date
case .serverTimestamp:
return Date()
}
}
}
class FirebaseDateTransform: TransformType {
public typealias Object = FirebaseDate
public typealias JSON = Any
open func transformFromJSON(_ value: Any?) -> FirebaseDate? {
switch value {
case let millisecondsSince1970 as Double:
let date = Date(millisecondsSince1970: millisecondsSince1970)
return .date(date)
case is [AnyHashable: Any]?:
return .serverTimestamp
default:
return nil
}
}
open func transformToJSON(_ value: FirebaseDate?) -> Any? {
switch value {
case .date(let date)?:
return date.millisecondsSince1970
case .serverTimestamp?:
return ServerValue.timestamp()
default:
return nil
}
}
}
You can get a date approximation from Firebase. For example if you're trying to change a firebase user's creation date (a Timestamp) to a Date:
user.creationDate.dateValue()
Swift 4 and updated Firebase library variation of Katfang's answer:
let currentTimeStamp: TimeInterval?
let ref = Database.database().reference().child("serverTimestamp")
ref.setValue(ServerValue.timestamp())
ref.observe(.value, with: { snap in
if let t = snap.value as? TimeInterval {
print(t/1000)
currentTimeStamp = t/1000
}
})