Trouble finding out if this counts as a read/many reads/will I get charged loads on database costs? - swift

I am currently developing an iOS app with a google cloud firestore as a backend and I am using a few listeners to find out if data is updated and then pushing it to my device accordingly. I wrote this function that listens for a value if true or not and according to so will update an animation in my app. The trouble is I don't know if I wrote it properly and don't want to incur unnecessary reads from my database if I don't have to.
func dingAnimation() {
let identifier = tempDic![kBOUNDIDENTIFIER] as! String
if identifier != "" {
dingListener = reference(.attention).document(identifier).addSnapshotListener({ (snapshot, error) in
if error != nil {
SVProgressHUD.showError(withStatus: error!.localizedDescription)
return
}
guard let snapshot = snapshot else { return }
let data = snapshot.data() as! NSDictionary
for dat in data {
let currentId = FUser.currentId() as! String
let string = dat.key as! String
if string == currentId {
} else {
let value = dat.value as! Bool
self.shouldAnimate = value
self.animateImage()
}
}
})
}
}

This might help you.
From Firestore DOCS - Understand Cloud Firestore billing
https://firebase.google.com/docs/firestore/pricing
Listening to query results
Cloud Firestore allows you to listen to the results of a query and get realtime updates when the query results change.
When you listen to the results of a query, you are charged for a read each time a document in the result set is added or updated. You are also charged for a read when a document is removed from the result set because the document has changed. (In contrast, when a document is deleted, you are not charged for a read.)
Also, if the listener is disconnected for more than 30 minutes (for example, if the user goes offline), you will be charged for reads as if you had issued a brand-new query.

Related

How can I make retrieving data from firestore cloud faster when refreshing the page [duplicate]

I am new in programming and in iOS development. I am trying to make an app using Firestore database from Firebase. I don't know if it is normal or not, but when I am trying to get a data from firestore database, it seems too long for me. I don't know if I make a mistake or not
here is my code to get all city data from firestore
reference :
import Foundation
import FirebaseFirestore
import Firebase
enum FirestoreCollectionReference {
case users
case events
case cities
private var path : String {
switch self {
case .users : return "users"
case .events : return "events"
case .cities : return "cities"
}
}
func reference () -> CollectionReference {
return Firestore.firestore().collection(path)
}
}
I use getAllCitiesDataFromFirestore method in CityKM class to get the city data that stored in firestore
class CityKM {
var name : String
var coordinate : GeoPoint
init (name: String , coordinate: GeoPoint ) {
self.name = name
self.coordinate = coordinate
}
init (dictionary: [String:Any]) {
// this init will be used if we get data from firebase observation to construct an event object
name = dictionary["name"] as! String
coordinate = dictionary["coordinate"] as! GeoPoint
}
static func getAllCitiesDataFromFirestore (completion: #escaping ( [CityKM]? )->Void) {
// to retrieve all cities data from Firebase database by one read only, not using realtime fetching listener
let startTime = CFAbsoluteTimeGetCurrent() // to track time consumption of this method
FirestoreCollectionReference.cities.reference().getDocuments { (snapshot, error) in
if let error = error {
print("Failed to retrieve all cities data: \(error.localizedDescription)")
} else {
print("Sucessfully get all cities data from firestore")
guard let documentsSnapshot = snapshot, !documentsSnapshot.isEmpty else {
completion(nil)
return
}
let citiesDocuments = documentsSnapshot.documents
var cityArray = [CityKM]()
for document in citiesDocuments {
guard let cityName = document.data()["name"] as? String,
let cityCoordinate = document.data()["coordinate"] as? GeoPoint else {return}
let theCity = CityKM(name: cityName, coordinate: cityCoordinate)
cityArray.append(theCity)
}
completion(cityArray)
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime // to track time consumption of this method
print("Time needed to get all cities data from Firestore : \(timeElapsed) s.") // to track time consumption of this method
}
}
}
}
extension CityKM {
// MARK: - User Helper Methods
func toDictionary() -> [String:Any]{
return [
"name" : name,
"coordinate" : coordinate
]
}
}
from my debugging area, it is printed
"Time needed to get all cities data from Firestore : 1.8787678903 s."
is it possible to make it faster? Is 1.8s normal? am i make a mistake in my code that make the request data takes too long time ? I hope that I can make request time is below one second
I don't think the internet speed is the problem, since I can open video on youtube without buffering
That performance sounds a bit worse than what I see, but nothing excessive. Loading data from the cloud simply takes time. A quick approach to hide that latency is by making use of Firebase's built-in caching.
When you call getDocuments, the Firebase client needs to check on the server what the document's value is before it can call your code, which then shows the value to the user. As said: there is no way to speed up this reading in your code, so it'll always take at least 1.8s before the user sees a document.
If instead you listen for realtime updates from the database with addSnapshotListener, the Firebase client may be able to immediately call your code with values from its local cache, and then later re-invoke your code in case there has been an update to the data on the server.

Swift Firebase get batches of documents in order

For context, I have a bunch of documents that hold fields similar to a social media post. (photo url link, like count, date uploaded, person who uploaded it, etc.) And I am showing this data in a gallery (lazyvgrid). I do not want to get all of the documents at once so when the user scrolls down the gallery I am getting 20 documents at a time based on how far the user scrolls down the gallery view. I am sorting my get request with:
self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20).getDocuments
I have no problem getting the first 20 using this code. How can I get the next 20 and the 20 after that, and so on?
With query cursors in Cloud Firestore, you can split data returned by a query into batches according to the parameters you define in your query.
Query cursors define the start and end points for a query, allowing you to:
Return a subset of the data.
Paginate query results.
Use the startAt() or startAfter() methods to define the start point for a query. Use the endAt() or endBefore() methods to define an endpoint for your query results.
As Dharmaraj mentioned for your case, it will be best if we use Pagination with Firestore.
Paginate queries by combining query cursors with the limit() method to limit the number of documents you would want to show in the gallery. And as you want no definite numbers, but the user should be able to scroll through as long as he wants, and as long as there are documents, I would suggest to put a cursor until the last document, like in the below code sample.
To get the last document,
let first = db.collection("collectionname")
.order(by: "fieldname")
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
I ended up referencing Dharmaraj's link in his comment.
#Published var isFetchingMoreDocs: Bool = false
private var lastDocQuery: DocumentSnapshot!
public func getUpdatedEventMedias(currentEventID: String, eventMedias: [EventMedia], completion: #escaping (_ eventMedias: [EventMedia]) -> Void) {
self.isFetchingMoreDocs = true
var docQuery: Query!
if eventMedias.isEmpty {
docQuery = self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20)
} else if let lastDocQuery = self.lastDocQuery {
docQuery = self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20).start(afterDocument: lastDocQuery)
}
if let docQuery = docQuery {
print("GET DOCS")
docQuery.getDocuments { (document, error) in
if let documents = document?.documents {
var newEventMedias: [EventMedia] = []
for doc in documents {
if let media = try? doc.data(as: EventMedia.self) {
newEventMedias.append(media)
}
}
self.lastDocQuery = document?.documents.last
self.isFetchingMoreDocs = false
completion(newEventMedias)
} else if let error = error {
print("Error getting updated event media: \(error)")
self.isFetchingMoreDocs = false
completion([])
}
}
} else {
self.isFetchingMoreDocs = false
completion([])
}
}
As seen in my code, by utilizing:
.order(by: "savesCount", descending: true).limit(to: 20).start(afterDocument: lastDocQuery)
I am able to start exactly where I left off. I should also note that I am only calling this function if !isFetchingMoreDocs - otherwise the func will be called dozens of times in a matter of seconds while scrolling. The most important thing about this code is that I am checking lastDocQuery if it is nil. After the user scrolls all the way to the bottom, the lastDocQuery will no longer be valid and cause a fatal error. Also I am using a custom scroll view that tracks the scroll offset in order to fetch more media and make more calls to firebase.

If I go to another view controller and return to the same one using a segue, does the snapshotListener reread all the documents?

This is a little snip it from my code.
db.collection("tasks").document(Auth.auth().currentUser?.uid ?? "").collection("currentUser").whereField("Date", isEqualTo: date).addSnapshotListener{ (querySnapshot, err) in
self.task = []
if querySnapshot!.documents.isEmpty{
self.firstView.alpha = 1
self.labelText.alpha = 1
}
else{
self.firstView.alpha = 0
self.labelText.alpha = 0
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
print("xx")
if let descS = data["Task Description"] as? String, let dateS = data["Date"] as? String, let titleS = data["Task Title"] as? String, let type1 = data["Type"] as? String{
let newTask = Tasks(title: titleS, desc: descS, date: dateS, type: type1, docId: doc.documentID)
self.task.append(newTask)
DispatchQueue.main.async {
self.taskTableView.reloadData()
let indexPath = IndexPath(row: self.task.count - 1, section: 0)
self.taskTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
The problem is that when I go to add another task I perform a segue to another view controller. From there I add a document but I need to perform another segue to go back because I am using a hamburger menu.
I did try using getDocuments(source: cache), which did reduce writes when the user did not add a task. But when they did add a task it reloads all the documents, adding tons of reads. The goal of using a snapshotListner is to reduce reads, however, I'm not sure if it will reread data when I perform a segue to the screen again. Thank-You!
The snapshot listener doesn't care at all about anything going on the UI, such as segues. The snapshot listener only fires when you first add it and when there are updates in the remote data. You can finetune the snapshot listener to omit or include cached or presumed data (using the snapshot's metadata property). But this is the nature of realtime data, it may get updated a lot as the user does things. The only workaround is coming up with the most efficient data architecture possible (on the server side) to reduce your read cost.
As an aside, your code is very dangerously written. You should first check if there is a valid userId before attaching a listener to a userId that even your code suggests could be an empty string. You should never force unwrap snapshots like you are doing because it will crash the entire app when there is even a slight network error.

How can I create a snapshot to listen to users being ADDED to my Firestore database?

I am using Firestore. I am trying to listen for when a new user is added. The problem is, each user also has a friends dictionary. So when I use a snapshot, my code is detecting both events of (1) A new user being added and (2) a new friend being added.
I have tries iterating over the document changes data and restricting doc.document.data()["friends"] == nil. Why isn't this working/how can I properly add a restriction to only include when a new user is added?
func observeUsers(onSuccess: #escaping(UserCompletion)) {
Ref().firestoreUserRef.collection("users").addSnapshotListener { (querySnapshot, error) in
if error != nil {
print("error with observeUser snapshot")
return
}
querySnapshot?.documentChanges.forEach { doc in
//I want to detect that a new user was added, I do not want to detect if a friend was added
if (doc.type == .added) && doc.document.data()["friends"] == nil {
guard let dict = querySnapshot else { return }
for document in dict.documents {
var dictionary = [String : Any]()
dictionary = document.data()
if let user = User.transformUser(dict: dictionary) {
onSuccess(user)
}
}
}
}
}
}
The easiest way is starting from the data model to support the types of queries you want. In this case, when you create a new user (which I assume always has no friends), set the friends field explicitly to null. This will let you query for all new users:
Ref().firestoreUserRef.collection("users").whereField("friends", isEqualTo: NSNull()).addSnapshotListener ...
An assumption here is your transformUser process will update the document and replace null with either a list of friends of an empty array so that it no longer matches the query.

Upvote/Downvote system within Swift via Firebase

I've looked over hours of code and notes and I'm struggling to find any documentation that would help me with upvoting and downvoting an object in a swift app with firebase.
I have a gallery of photos and I'm looking to add an instagram style upvote to images. The user has already logged with firebase auth so I have their user ID.
I'm just struggling to figure the method and what rules need to be set in firebase.
Any help would be awesome.
I will describe how I implemented such a feature in social networking app Impether using Swift and Firebase.
Since upvoting and downvoting is analogous, I will describe upvoting only.
The general idea is to store a upvotes counter directly in the node corresponding to an image data the counter is related to and update the counter value using transactional writes in order to avoid inconsistencies in the data.
For example, let's assume that you store a single image data at path /images/$imageId/, where $imageId is an unique id used to identify a particular image - it can be generated for example by a function childByAutoId included in Firebase for iOS. Then an object corresponding to a single photo at that node looks like:
$imageId: {
'url': 'http://static.example.com/images/$imageId.jpg',
'caption': 'Some caption',
'author_username': 'foobarbaz'
}
What we want to do is to add an upvote counter to this node, so it becomes:
$imageId: {
'url': 'http://static.example.com/images/$imageId.jpg',
'caption': 'Some caption',
'author_username': 'foobarbaz',
'upvotes': 12,
}
When you are creating a new image (probably when an user uploads it), then you may want to initialize the upvote counter value with 0 or some other constant depending on what are you want to achieve.
When it comes to updating a particular upvotes counter, you want to use transactions in order to avoid inconsistencies in its value (this can occur when multiple clients want to update a counter at the same time).
Fortunately, handling transactional writes in Firebase and Swift is super easy:
func upvote(imageId: String,
success successBlock: (Int) -> Void,
error errorBlock: () -> Void) {
let ref = Firebase(url: "https://YOUR-FIREBASE-URL.firebaseio.com/images")
.childByAppendingPath(imageId)
.childByAppendingPath("upvotes")
ref.runTransactionBlock({
(currentData: FMutableData!) in
//value of the counter before an update
var value = currentData.value as? Int
//checking for nil data is very important when using
//transactional writes
if value == nil {
value = 0
}
//actual update
currentData.value = value! + 1
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
error, commited, snap in
//if the transaction was commited, i.e. the data
//under snap variable has the value of the counter after
//updates are done
if commited {
let upvotes = snap.value as! Int
//call success callback function if you want
successBlock(upvotes)
} else {
//call error callback function if you want
errorBlock()
}
})
}
The above snipped is actually almost exactly the code we use in production. I hope it helps you :)
I was very surprised, but this code from original docs works like a charm. There is one disadvantage with it: the json grows pretty big if there are a lot of likes.
FirebaseService.shared.databaseReference
.child("items")
.child(itemID!)
.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if var item = currentData.value as? [String : AnyObject] {
let uid = SharedUser.current!.id
var usersLikedIdsArray = item["liked_who"] as? [String : Bool] ?? [:]
var likesCount = item["likes"] as? Int ?? 0
if usersLikedIdsArray[uid] == nil {
likesCount += 1
usersLikedIdsArray[uid] = true
self.setImage(self.activeImage!, for: .normal)
self.updateClosure?(true)
} else {
likesCount -= 1
usersLikedIdsArray.removeValue(forKey: uid)
self.setImage(self.unactiveImage!, for: .normal)
self.updateClosure?(false)
}
item["liked_who"] = usersLikedIdsArray as AnyObject?
item["likes"] = likesCount as AnyObject?
currentData.value = item
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
self.owner?.show(error: error)
}
}
Not a Swift fella myself (pun!) but I think this stackoverflow question has most of your answers.
Then you would simply use a couple of if statements to return the correct value from the transaction based on whether you want to up vote or down vote.