Not able to convert the Realtime Database code to Firestore code - ios

I am trying to configure the database for the firestore database, but not able to do so, facing several errors, and not getting any particular solution for that, I am using swift for ios, it is basically a chat app followed from the codelab tutorial, here's the link to the same tutorial! this tutorial is based on Firebase Realtime database, and my requirement is of Firestore database,
thanks in advance for the guidance
What I Tried for converting to firestore database
var messages: [DocumentSnapshot]! = []
fileprivate var _refHandle: CollectionReference!
var ref = Firestore.firestore().collection("messages")
func configureDatabase() {
//ref = Firestore.firestore() // Listen for new messages in the Firebase database
_refHandle = self.ref.addSnapshotListener{ querySnapshot, error in
if let documents = querySnapshot?.documents {
var messages = [DocumentSnapshot]()
for document in documents {
let message = messages(snapshot: document)
message.append(message)
message.clientTable.insertRows(at: [IndexPath(row: message.messages.count-1, section: 0)], with: .automatic)
}
// completion(messages)
}
} as! CollectionReference
}
RealtimeDatabase Code
var ref: DatabaseReference!
var messages: [DataSnapshot]! = []
fileprivate var _refHandle: DatabaseHandle?
deinit {
if let refHandle = _refHandle {
self.ref.removeObserver(withHandle: _refHandle)
}
}
func configureDatabase() {
ref = Database.database().reference()
// Listen for new messages in the Firebase database
_refHandle = self.ref.child("messages").observe(.childAdded, with: { [weak self] (snapshot) -> Void in
guard let strongSelf = self else { return }
strongSelf.messages.append(snapshot)
strongSelf.clientTable.insertRows(at: [IndexPath(row: strongSelf.messages.count-1, section: 0)], with: .automatic)
})
}
Error in firestore Code
Cannot call value of non-function type '[DocumentSnapshot]'"
at
let message = messages(snapshot: document)

This may or may not help but here's a function that reads in all messages and prints the message id (documentId) and the message text to console. Assume we have a class var messsagesArray set up to store each message and a class that hold the documentId and the message text.
Assume this structure
messages //the collection
msg_id_0 //the document id
msgText "Hello"
msg_id_1
msgText:"World"
and the code to read and print the messages
func readAllMessages() {
let messagesCollection = self.db.collection("messages")
messagesCollection.getDocuments(completion: { snapshot, err in
if let err = err {
print(err.localizedDescription)
return
}
for doc in snapshot!.documents {
let key = doc.documentID
let msg = doc.get("msg") as! String
let aMsg = MessageClass(aKey: key, msgText: msg)
self.messagesArray.append(aMsg)
}
for msg in self.messagesArray { //output the read messages to console
print(msg.key, msg.msgText)
}
})
}

Related

Listen for documents just when actually added to collection - swift

I have a collection on Firestore and I listen for changes like this:
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
})
}
I only want to listen for documents that are actually added to that collection.
In fact, the problem is that whenever I invoke this function I receive all the documents of the collection as added documents and then I also receive documents added later.
How can I listen just for actually added later documents, ignoring the ones already present in the collection? Searching online I didn't find any solution to this issue.
EDIT:
This is the way I tried to solve the problem:
func createMatchesListener(){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
getUidsAlreadyMade { uidsAlreadyMade in
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
let data = change.document.data()
let userId = data["uid"] as? String ?? ""
if uidsAlreadyMade.contains(userId) == false{
//means the uid is newly created in the collection, do stuff accordingly
arrayOfUidsAlreadyMade.append(currentUid)
}
}
if change.type == .removed{
// if the document has been removed, remove also the id from the array of uids
let data = change.document.data()
let currentUid = data["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.removeAll { $0 == currentUid }
}
})
})
}
}
func getUidsAlreadyMade(completion: #escaping ([String]) -> Void){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
db.collection("Matches").document(currentUid).collection("Matches").getDocuments { snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
arrayOfUidsAlreadyMade.removeAll()
snapshot?.documents.forEach({ doc in
let dict = doc.data()
let userId = dict["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.append(userId)
})
completion(arrayOfUidsAlreadyMade)
}
}
A simple solution is to include a timestamp in your Firestore documents.
Suppose your documents store Tasks, for example
documentId
task: "get dinner"
timestamp: 20211123
and suppose your app doesn't care about past tasks, only new ones.
When the tasks are read, update the timestamp as to when that occurred.
Then each time after that you want to read only 'new data' specify that in your listener, keeping track of when the last read timestamp was:
db.collection("task").whereField("timestamp", isGreaterThan: lastReadTimeStamp).addSnapshotListener...
The above will only read in tasks that occured after the prior timestamp and add a Listener (reading in all of the new tasks so you can populate the UI).
You can store an array with the ID of the documents that you already have stored in the device. That way, all that you need to do before doing things is checking that document's id is not in your array
There's no way of preventing Firestore from returning the initial snapshot of documents when a document listener is added, so just use a boolean to keep track of the initial snapshot and ignore it.
var listenerDidInit = false
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
if listenerDidInit {
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
} else {
listenerDidInit = true
}
})
}
private var listener: ListenerRegistration?
self.listener = db.collection("Matches") // matchesListener
listener!.remove()

Can't pass variable value from firebase firestore to another class SWIFT

So I have this function in class Functions :
struct Prices {
var standardPrice: Int!
}
// FUNC PRICING
class Functions {
private var PricingRef: CollectionReference!
var price = Prices()
func getPrice() -> Prices {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
}
}
}
return price
}
}
Then I want to pass the standardPrice value to this class, called PriceList :
class PriceList: UITableViewController {
var price = Prices()
var newStandardPrice = 0
func Price() {
price = Functions().getPrice()
newStandardPrice = price.standardPrice // always error with value nil
}
I always have that error where newStandardPrice is nil.
but the print(self.price.standardPrice!) shows number of result I want.
So as far as I know, the problem here is because it takes time for the firebase firestore to get the data from database.
How do I get the value of standardPrice after its assigned with the new price from firebase database?
Any help will be appreciated
Thankyou
you need to use completion handler because its async function
func getPrice(completion:#escaping (Prices?,Error?)-> Void) {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
completion(nil,err)
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
completion(self.price.standardPrice,nil)
}
}
}
}
How to use
Functions().getPrice { (price, error) in
if let err = error {
// do something if you get error
} else if let getPrice = price {
// use price
self.price = getPriice
}

not able retrieve documents in chat application according to the timestamp

I am using the below code to retrieve the messages in a chat application according to the timestamp but it is not retrieving in order of the timestamp, How should I make sure that messages retrieved are in the order of the timestamp.
I am using Firestore database and Swift IOS for this application
below is the code parts
timestamp saved in database
let timestamp = Int(NSDate().timeIntervalSince1970)
Code to retrieve messages
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true)
ref.addSnapshotListener { (snapshot, error) in
snapshot?.documentChanges.forEach({ (diff) in
let messageId = diff.document.documentID
let messageRef = Firestore.firestore().collection("messages")
.document(messageId)
messageRef.getDocument(completion: { (document, error) in
guard let dictionary = document?.data() as? [String : Any] else { return }
let message = Message(dictionary: dictionary)
print("we fetched this message \(message.text)")
self.messages.append(message)
DispatchQueue.main.async {
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
})
})
}
Perhaps an oversight but what's happening here is the code gets the data you want in descending order by timestamp, but then gets that same data again, which will be unordereed because it's being retrieved asynchronously, and adds to the array.
func doubleGettingData() {
let ref = Firestore.firestore()....
Gets data -> ref.addSnapshotListener { (snapshot, error) in
snapshot?.documentChanges.forEach({ (diff) in
Gets data again -> messageRef.getDocument(completion
To add a bit more context, the 'outside' function shown in the question is in fact getting the documents in the correct order. However, getting those same documents again, they are being returned from Firebase in whatever order they complete in because Firebase calls are asynchronous. This can be proven if we remove all the code except for the two calls. Here's an example Firestore Structure
message_0:
timestamp: 2
message_1
timestamp: 0
message_2
timestamp: 1
and when some print statement are added, here's what's happening
outside func gets: message_0 //timestamp 2
outside func gets: message_2 //timestamp 1
outside func gets: message_1 //timestamp 0
inside func returns: message_1 //timestamp 0
inside func returns: message_2 //timestamp 1
inside func returns: message_0 //timestamp 2
I would make a couple of changes...
Here's my Message class and the array to store the messages in
class Message {
var text = ""
var timestamp = ""
convenience init(withSnap: QueryDocumentSnapshot) {
self.init()
self.text = withSnap.get("text") as? String ?? "No message"
self.timestamp = withSnap.get("timestamp") as? String ?? "No Timestamp"
}
}
var messages = [Message]()
and then the code to read the messages, descending by timestamp and store them in the array. Note
The first query snapshot contains added events for all existing
documents that match the query
func readMessages() {
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true)
ref.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let snap = diff.document
let aMessage = Message(withSnap: snap)
self.messages.append(aMessage)
}
if (diff.type == .modified) {
let docId = diff.document.documentID
//update the message with this documentID in the array
}
if (diff.type == .removed) {
let docId = diff.document.documentID
//remove the message with this documentID from the array
}
}
}
}
This code will also watch for changes and deletions in messages and pass that event to your app when they occur.

Error when QuerySnapshot runs in Firestore - Throws error Skipping epollex becuase skipping EPOLLEX GRPC_LINUX_EPOLL is not defined

I have created a function to pull a random document from my firestore collection, which then returns a FIRQuery. Once i got that, I created a model file to parse any FIRQuery data from my firestore document. However, once i run it and attempt to print it to the log, I get the response of "Skipping epollsig becuase GRPC_LINUX_EPOLL is not defined." 4 times, which makes sense as i am trying to print 4 fields of my document. My code for the parsing is below - keep in mind any CAPS values are constants that are just defined as equal to exactly how they read in the firestore document field
class randomModel {
private(set) var quote: String!
private(set) var randomInt: Int!
private(set) var approved: Bool!
private(set) var source: String!
init(approved: Bool, quote: String, randomInt: Int, source: String)
{
self.approved = approved
self.quote = quote
self.randomInt = randomInt
self.source = source
}
class func parseData(snapshot: QuerySnapshot?) -> [randomModel] {
var quoteStructure = [randomModel]()
guard let snap = snapshot else {return quoteStructure}
for document in (snap.documents) {
let data = document.data()
let quote = data[DATABASE_QUOTE_VALUE_FIELD] as? String
let randomInt = data[DATABASE_RANDOM_INTEGER_VALUE_FIELD] as? Int
let approved = data[DATABASE_APPROVED_VALUE_FIELD] as? Bool
let source = data[DATABASE_SOURCE_VALUE_FIELD] as? String
let parsedData = randomModel(approved: approved!, quote: quote!, randomInt: randomInt!, source: source!)
quoteStructure.append(parsedData)
}
return quoteStructure
}
Then my code to actually print the data and pull a random document is here:
#IBAction func generatePressed(_ sender: Any) {
let quoteRef = Firestore.firestore().collection("Quotes")
let queryRef = quoteRef.whereField(DATABASE_RANDOM_INTEGER_VALUE_FIELD, isGreaterThan: 1)
.order(by: "randomInt")
.limit(to: 1)
.addSnapshotListener { (snapshot, error) in
if let err = error {
debugPrint("Error Fetching Document \(err)")
} else {
print(randomModel.parseData(snapshot: snapshot))
}
}
}
So can someone guide me as to what I am doing wrong to throw this error. My Document structure looks like this in firestore.. sorry, I don't have any rep to be able to embed into a post yet. Thanks for the help in advance!!
This is a bug that was fixed in gRPC 1.8.4. Run pod update.

Swift iOS: Firebase Paging

I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}

Resources