Firestore Listener Document Changes - swift

I am trying to create a Listener for changes to a Document. When I change the data in Firestore (server) it doesn't update in the TableView (App). The TableView only updates when I reopen the App or ViewController.
I have been able to set this up for a Query Snapshot but not for a Document Snapshot.
Can anyone look at the code below to see why this is not updating in realtime?
override func viewDidAppear(_ animated: Bool) {
var newDocIDString = newDocID ?? ""
detaliPartNumberListerner = firestore.collection(PARTINFO_REF).document(newDocIDString).addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
self.partInfos.removeAll()
self.partInfos = PartInfo.parseData2(snapshot: documentSnapshot)
self.issueTableView.reloadData()
}
In my PartInfo file
class func parseData2(snapshot: DocumentSnapshot?) -> [PartInfo] {
var partNumbers = [PartInfo]()
guard let snap = snapshot else { return partNumbers }
//for document in snap.documents {
// let data = document.data()
let area = snapshot?[AREA] as? String ?? "Not Known"
let count = snapshot?[COUNT] as? Int ?? 0
//let documentId = document.documentID
let documentId = snapshot?.documentID ?? ""
let newPartInfo = PartInfo(area: area, count: count, documentId: documentId)
partNumbers.append(newPartInfo)
return partNumbers
}

UI work must always be done on the main thread. So instead of your last line in your first code snippet, do this:
DispatchQueue.main.async {
self.issueTableView.reloadData()
}
I think this might be the solution to your problem. (A little late, I know ...)

Related

Firestore Swift update text realtime

I have this way of collecting information.
struct MainText {
var mtext: String
var memoji: String
}
class MainTextModel: ObservableObject {
#Published var maintext : MainText!
init() {
updateData()
}
func updateData() {
let db = Firestore.firestore()
db.collection("maintext").document("Main").getDocument { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
let memoji = snap?.get("memoji") as! String
let mtext = snap?.get("mtext") as! String
DispatchQueue.main.async {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}
}
}
And such a way of displaying.
#ObservedObject private var viewModel = MainTextModel()
self.viewModel.maintext.memoji
self.viewModel.maintext.mtext
How can I update online without rebooting the view?
Instead of using getDocument, which only gets the document once and doesn't return updates, you'll want to add a snapshot listener.
Here's the Firestore documentation for that: https://firebase.google.com/docs/firestore/query-data/listen
In your case, you'll want to do something like:
db.collection("maintext").document("Main")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
if let memoji = data["memoji"] as? String, let mtext = data["mtext"] as? String {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}

How to retrieve data from Firestore as soon as SwiftUI view appears?

I have this class CurrentUser that manages the currently logged in user and pulls the data for that user from Firebase.
One of CurrentUser's attributes is userEventIDs. I also have a collection of Events documents. Each user has their own array of event IDs that correspond to the events within the Events collection in my Firestore database.
On the MyAccount view struct I have an onAppear method that queries the Events collection based on the currentUser's array of eventIds, returns those Events, and then sorts them to be either before or after today based on the date of the event.
Currently the eventIds load in the first time this view is opened, but the query from the events comes back blank twice and only after the view is switched to another one and back to the MyAccount view will the page populate with these events.
Is there something I can do to make the events load on the first time the view is opened?
CurrentUser
class CurrentUser: ObservableObject {
let user = Auth.auth().currentUser
#Published var currentUserInformation = User(id: "", name: "", email: "'", accountType: "", profPicURL: "", coverPhotoURL: "", numberFollowers: nil, description: nil, location: nil, websiteLink: nil, orgID: nil, userEventIDs: [String](), userEvents: [Event]())
init() {
getUserInformation()
}
func getUserInformation() {
let UID = user!.uid
let database = Firestore.firestore()
database.collection("Organizers").whereField("Organizer ID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
self.currentUserInformation.name = document.get("Organization Name") as! String
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = document.get("Account Type") as! String
self.currentUserInformation.profPicURL = document.get("Profile Pic URL") as! String
self.currentUserInformation.coverPhotoURL = document.get("Cover Pic URL") as! String
self.currentUserInformation.numberFollowers = (document.get("Number of Followers") as! Int)
self.currentUserInformation.description = (document.get("Organization Description") as! String)
self.currentUserInformation.websiteLink = (document.get("Organization Website Link") as! String)
self.currentUserInformation.location = (document.get("Organization Location") as! String)
self.currentUserInformation.orgID = (document.get("Organizer ID") as! String)
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
self.currentUserInformation.accountType = "Organizer"
}
}
if self.currentUserInformation.id == "" {
database.collection("Activists").whereField("UID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
let firstName = document.get("First Name") as! String
let lastName = document.get("Last Name") as! String
self.currentUserInformation.name = "\(firstName) \(lastName)"
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = "Activist"
self.currentUserInformation.profPicURL = document.get("Profile Pic") as! String
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
}
}
}
}
func getUserEvents() {
let database = Firestore.firestore()
let eventRef = database.collection("Events")
for eventID in self.currentUserInformation.userEventIDs {
for event in self.currentUserInformation.userEvents {
if event.id == eventID {
break
}
}
eventRef.document(eventID).getDocument() { (document, error) in
if let document = document {
let id = document.documentID
let eventTitle = document.get("Name") as! String
let organizer = document.get("Organizer") as! String
let organizerID = document.get("Organizer ID") as! String
let eventDescription = document.get("Description") as! String
let date = document.get("Date") as! String
let time = document.get("Time") as! String
let location = document.get("Location") as! String
let numAttending = document.get("Number Attending") as! Int
let eventPhotoURL = document.get("Event Photo URL") as! String
self.currentUserInformation.userEvents.append(Event(id: id, eventTitle: eventTitle, eventOrganizer: organizer, eventOrganizerID: organizerID, eventDescription: eventDescription, date: date, time: time, location: location, numAttending: numAttending, eventPhotoURL: eventPhotoURL))
} else {
print("Document does not exist")
}
}
}
}
}
View
.onAppear() {
if currentActivist.currentUserInformation.userEvents.count != currentActivist.currentUserInformation.userEventIDs.count {
currentActivist.getUserEvents()
print("Getting user events")
}
pastEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Past"]!
futureEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Upcoming"]!
}
A couple of quick notes:
Most Firebase calls are asynchronous (check out this article to understand why), so your call to Auth.auth().currentUser is most likely going to return nil. Instead, you should register an AuthenticationStateListener. See this sample code to see how it's done.
Instead of instantiating an empty User instance, make currentUserInformation optional
Mapping data is much easier using Firestore's support for Codable. I've written extensively about this, but the gist of it is, you'll be able to map documents with a single line of code (instead of having to manually map every single field). The Firestore documentation actually has a nice code snippet that you can adopt:
let docRef = db.collection("cities").document("BJ")
docRef.getDocument { (document, error) in
// Construct a Result type to encapsulate deserialization errors or
// successful deserialization. Note that if there is no error thrown
// the value may still be `nil`, indicating a successful deserialization
// of a value that does not exist.
//
// There are thus three cases to handle, which Swift lets us describe
// nicely with built-in Result types:
//
// Result
// /\
// Error Optional<City>
// /\
// Nil City
let result = Result {
try document?.data(as: City.self)
}
switch result {
case .success(let city):
if let city = city {
// A `City` value was successfully initialized from the DocumentSnapshot.
print("City: \(city)")
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding city: \(error)")
}
}
Avoid force unwrapping (using the ! operator), use optional unwrapping (using the ? operator), and the nil-coalescing operator (??) instead

I want my code to run consecutively/synchronously in the background (DispatchQueue)

I want grabAllFollowingPosts() to run only after loadFollowing() has finished running. These are both network calls so I want to run them in the background. Any ideas on why my code isn’t working?
DispatchQueue.global(qos: .userInteractive).sync {
self.loadFollowing()
self.grabAllFollowingPosts()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
What these 3 functions do is:
grab every user that the current user is following
for each of those users, grab their posts
Hence, loadUsers() must run before grabAllFollowingPosts()
var followingUsers = [String]()
//Function 1: load the poeple you are following into the followingUsers array
func loadFollowing () {
guard let userID = Auth.auth().currentUser?.uid else { return }
let firestoreRef = Firestore.firestore().collection("Following").document(userID).collection("UserFollowing")
firestoreRef.addSnapshotListener { (snapshot, error) in
if error != nil {
//error retrieving documents
print (error!.localizedDescription)
} else {
// document retrival successful
guard let snapshot = snapshot else { return }
for document in snapshot.documents {
let data = document.data()
let userid = data["UserID"] as? String ?? "anonymous"
self.followingUsers.append(userid)
}
}
}
}
//Function 2: for all of the users in the followingUsers array - grab their documents from Firestore
func grabAllFollowingPosts () {
for users in followingUsers {
loadPosts(theUsers: users)
}
}
//Function 3: loads the posts
func loadPosts (theUsers: String) {
let firestoreRef = Firestore.firestore().collection("Posts").whereField("UserID", isEqualTo: theUsers).whereField("Date", isGreaterThanOrEqualTo: Date()).limit(to: 8)
//TODO: add infinate scroll
firestoreRef.addSnapshotListener { (snapshot, error) in
if error != nil {
//error retrieving documents
print (error!.localizedDescription)
} else {
// document retrival successful
guard let snapshot = snapshot else { return }
for document in snapshot.documents {
let data = document.data()
let ageRestriction = data["AgeRestriction"] as? String ?? "Age E"
let category = data["Category"] as? String ?? "Error - No Category"
let date = data["Date"] as? Date ?? Date()
let documentId = data["DocumentID"] as? String ?? "Error - No Document-ID"
let description = data["Description"] as? String ?? "Error - No Description"
let location = data["Location"] as? String ?? "Error - No Location"
let title = data["Title"] as? String ?? "Error - No Title"
let userId = data["UserID"] as? String ?? "Error - No User-ID"
let username = data["Username"] as? String ?? "Anonymous"
let color = data["Color"] as? String ?? "Sale"
let newPost = Post(documentIDText: documentId, usernameText: username, titleText: title, locationText: location, dateText: date, descriptionText: description, ageText: ageRestriction, category: category, uid: userId, color: color)
self.posts.append(newPost)
}
if self.posts.isEmpty {self.goFollowPeopleImage.isHidden = false}
}
}
}
There are two basic patterns:
When dealing with RESTful network requests, we give all of our network routines a completion handler closure, which we call when the network request is done. That way, the caller can invoke each subsequent step in the completion handler of the prior step.
There are many variations on this theme (asynchronous Operation subclasses, futures/promises, etc), but the idea is the same, namely chaining a series of asynchronous tasks together in such a way that the caller can know when the requests are all done and can trigger the UI update.
On the other hand, when dealing with Firestore, we can add observers/listeners to update our UI as updates come in. The addSnapshotListener closure is repeatedly called as the underlying database is updated. In this scenario there isn’t a “ok, we’re done, update the UI” point in time (so we wouldn’t generally use the completion handler approach), but rather we just continually update the UI as the documents come in.
But while your example is using addSnapshotListener, it also is using the limit(to:), which adds a wrinkle. It’s a bit like the first scenario (e.g., if you’re limited to 8, and you retrieved 8, the listener won’t get called again). But it’s also a bit like the second scenario (e.g., if limiting to 8 and you currently have only 7 posts, it will retrieve the first seven and call that closure; but if another record comes in, it will call the closure again, this time with the 8th document as well).
Trying to handle both limited/paginated responses and listening for realtime updates can get complicated. I might suggest that if you want to make Firestore act like a RESTful service, I might suggest using getDocuments instead of addSnapshotListener, eliminating this complexity. Then you can use the completion handler approach recommended by others. It makes it behave a bit like the RESTful approach (but, then again, you lose the realtime update feature).
In case you’re wondering what the realtime, second scenario might look like, here is a simplified example (my post only has “text” and “date” properties, but hopefully it’s illustrative of the process):
func addPostsListener() {
db.collection("posts").addSnapshotListener { [weak self] snapshot, error in
guard let self = self else { return }
guard let snapshot = snapshot, error == nil else {
print(error ?? "Unknown error")
return
}
for diff in snapshot.documentChanges {
let document = diff.document
switch diff.type {
case .added: self.add(document)
case .modified: self.modify(document)
case .removed: self.remove(document)
}
}
}
}
func add(_ document: QueryDocumentSnapshot) {
guard let post = post(for: document) else { return }
let indexPath = IndexPath(item: self.posts.count, section: 0)
posts.append(post)
tableView.insertRows(at: [indexPath], with: .automatic)
}
func modify(_ document: QueryDocumentSnapshot) {
guard let row = row(for: document) else { return }
guard let post = post(for: document) else { return }
posts[row] = post
tableView.reloadRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
}
func remove(_ document: QueryDocumentSnapshot) {
guard let row = row(for: document) else { return }
posts.remove(at: row)
tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
}
func row(for document: QueryDocumentSnapshot) -> Int? {
posts.firstIndex {
$0.id == document.documentID
}
}
func post(for document: QueryDocumentSnapshot) -> Post? {
let data = document.data()
guard
let text = data["text"] as? String,
let timestamp = data["date"] as? Timestamp
else {
return nil
}
return Post(id: document.documentID, text: text, date: timestamp.dateValue())
}
But this approach works because I’m not limiting the responses. If you do use limit(to:) or limit(toLast:), then you’ll stop getting realtime updates when you hit that limit.

Retrieving all documents from collection in firebase

Database Structure
futsal_list.document(futsal_uid).collection("book_info").document(date in format "May 9, 2019".collection("newrequest").document(userUid)
and in useruid there is map like
time[
6AM : timestamp
7AM : timstamp,
]
I am using google cloud firestore and i want to retrieve all the new request from all the dates and it need to be realtime listener. I also want to divide the table view section according to the date. And i am only showing the today and upcoming dates and no past dates.
problem -
when a new request arrived the table add the single request multiple times in the table and when i go back and come to the view then the data shown are fine.
this is what i have tried
func loadAllRequestFromFirebasee() {
db.collection("futsal_list").document(currentUser!.uid).collection("book_info").addSnapshotListener { (querySnapshot, error) in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
self.newRequestArray.removeAll()
self.sectionHeader.removeAll()
var validDates = [String] ()
for document in snapshot.documents {
let bookedDate = document.documentID
if !self.isPastDate(date: bookedDate) {
validDates.append(bookedDate)
}
}
for (bookedDateIndex, bookedDate) in validDates.enumerated() {
self.db.collection("futsal_list").document(self.currentUser!.uid).collection("book_info").document(bookedDate).collection("newrequest").getDocuments(completion: { (uidSnapshots, error) in
guard let uidSnapshot = uidSnapshots else {
print("Error fetching snapshots: \(error!)")
return
}
let userUids = uidSnapshot.documents
if userUids.count > 0 {
self.newRequestArray.append([])
self.sectionHeader.append(bookedDate)
for document in userUids {
print("\(document.documentID) => \(document.data())")
let dataDescription = document.data()
let userUid = document.documentID
let bookedTimes = dataDescription["time"] as! [String : Any]
self.db.collection("users_list").document(userUid).getDocument(completion: { (document, error) in
if let document = document, document.exists {
let data = document.data()
let userName = data!["user_full_name"] as? String ?? ""
// let futsalAddress = data!["futsal_address"] as? String ?? ""
let userPhone = data!["user_phone_number"] as? String ?? ""
let userProfilePic = data!["user_profile_image"] as? String ?? ""
for time in bookedTimes.keys {
let newRequest = NewRequest(userUid: userUid, userName: userName, bookDate: bookedDate, bookTime: time, userPhoneNumber: userPhone, userProfilePicture: userProfilePic)
self.newRequestArray[bookedDateIndex].append(newRequest)
self.newBookRequestTableView.reloadData()
}
}
})
}
}
})
}
}
}

How can I retrieve users posts, add them with snapshots to table view - Swift

So I am trying to retrieve users follower information within an array. Then with that array get each users posts and then append them in my table view. All throughout this, I would like a snapshot listener to be added so that if a user likes a post the number will auto update. When I do this tho it appends every single update so one post will be shown about 5 times after an action such as liking it is performed which I do not want to happen. Could someone help me figure this out? I am using Xcode Swift. Thanks in advance!
class Posts {
var postArray = [UserPost]()
var db: Firestore!
init() {
db = Firestore.firestore()
}
func loadData(completed: #escaping () -> ()) {
let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
self.postArray = []
guard let user = Auth.auth().currentUser else { return }
let displayUsername = user.displayName
let userReference = db.collection("Users").document("User: \(displayUsername!)").collection("Connect").document("Following")
userReference.getDocument { (document, error) in
if let documentData = document?.data(),
var FollowerArray = documentData["Following"] as? [String] {
FollowerArray.append(displayUsername!)
FollowerArray.forEach {
self.db.collection("Users").document("User: \($0)").collection("Posts").whereField("timeOfPost", isGreaterThanOrEqualTo: sevenDaysAgo!)
.addSnapshotListener { (querySnapshot, error) in
guard error == nil else {
print("*** ERROR: adding the snapshot listener \(error!.localizedDescription)")
return completed()
}
//self.postArray = []
// there are querySnapshot!.documents.count documents in the posts snapshot
for document in querySnapshot!.documents {
let post = UserPost(dictionary: document.data())
self.postArray.append(post)
}
completed()
}
}
}
I would suggest a different approach and enable Firestore to tell you when child nodes (posts) have been added, modified or removed. Based on your code your structure is something like this:
Users
uid
//some user data like name etc
Posts
post_0
likes: 0
post: "some post 0 text"
post_1
likes: 0
post: "text for post 1"
Let's have a class to store the Post in
class UserPostClass {
var postId = ""
var postText = ""
var likes = 0
init(theId: String, theText: String, theLikes: Int) {
self.postId = theId
self.postText = theText
self.likes = theLikes
}
}
and then an array to hold the UserPosts which will be the tableView dataSource
var postArray = [UserPostClass]()
then.. we need a block of code to do three things. First, when a new post is added to the database (or when we first start the app), add it to the dataSource array. Second, when a post is modified, for example another user likes the post, update the array to reflect the new like count. Third, if a post is deleted, remove it from the array. Here's the code that does all three......
func populateArrayAndObservePosts() {
let uid = "uid_0" //this is the logged in user
let userRef = self.db.collection("users").document(uid)
let postsRef = userRef.collection("Posts")
postsRef.addSnapshotListener { documentSnapshot, error in
guard let snapshot = documentSnapshot else {
print("err fetching snapshots")
return
}
snapshot.documentChanges.forEach { diff in
let doc = diff.document
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
if (diff.type == .added) { //will initially populate the array or add new posts
let aPost = UserPostClass(theId: postId, theText: postText, theLikes: numLikes)
self.postArray.append(aPost)
}
if (diff.type == .modified) { //called when there are changes
//find the post that was modified by it's postId
let resultsArray = self.postArray.filter { $0.postId == postId }
if let postToUpdate = resultsArray.first {
postToUpdate.likes = numLikes
}
}
if (diff.type == .removed) {
print("handle removed \(postId)")
}
}
//this is just for testing. It prints all of the posts
// when any of them are modified
for doc in snapshot.documents {
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
print(postId, postText, numLikes)
}
}
}