Make UI Wait for Method Response - swift

I am a new swift developer. I am using Swift 4.2 and Xcode 10.2.
I need my UI to wait until a method has finished so I can use the result to display a balance. I am trying to use a dispatchGroup for this, but it does not appear to be waiting because the value of user?.userId below is nil. Here is my code:
// Load the local user data. Must wait until this is done to continue.
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
let user = LocalStorageService.loadCurrentUser()
dispatchGroup.leave()
// Display the current balance.
// Get a reference to the Firestore database.
let db = Firestore.firestore()
// Make sure we have a userId and then update the balance with a listener that keeps it updated.
// Only run this part when the dispatchGroup has completed (in this case, the user is loaded).
dispatchGroup.notify(queue: .main) {
if let userId = user?.userId {
db.collection("subs").whereField("ID", isEqualTo: userId)
.addSnapshotListener { querySnapshot, error in
// Make sure we have a document
guard let document = querySnapshot?.documents.first else {
print("Error fetching document: \(error!)")
return
}
// We have a document and it has data. Use it.
self.balance = document.get("balance") as! Double
// Format the balance
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
let balanceString = currencyFormatter.string(from: self.balance as NSNumber)
self.balanceLabel.setTitle(balanceString, for: .normal)
}
}
}
How can I make the UI wait until the method called in dispatchGroup.enter() has completed?
Here's what's in LoadCurrentUser....
static func loadCurrentUser() -> User? {
// Loads the current user in the UserDefaults if there is one
let defaults = UserDefaults.standard
let userId = defaults.value(forKey: Constants.LocalStorage.storedUserId) as? String
let phoneNumber = defaults.value(forKey: Constants.LocalStorage.storedPhoneNumber) as? String
let subscriberId = defaults.value(forKey: Constants.LocalStorage.storedDocumentId) as? String
guard userId != nil && phoneNumber != nil && subscriberId != nil else {
return nil
}
// Return the user
let u = User(userId:userId!, phoneNumber:phoneNumber!, subscriberId: subscriberId)
return u
}

Currently you do it correctly by setting vars inside the callback so no need for DispatchGroup , but to correctly use it then do ( notice the correct place where each line should be by numbers from 1 to 4 )
let dispatchGroup = DispatchGroup() /// 1
let user = LocalStorageService.loadCurrentUser()
// Display the current balance.
// Get a reference to the Firestore database.
let db = Firestore.firestore()
var balance = ""
// Make sure we have a userId and then update the balance with a listener that keeps it updated.
// Only run this part when the dispatchGroup has completed (in this case, the user is loaded).
if let userId = user?.userId {
dispatchGroup.enter() /// 2
db.collection("subs").whereField("ID", isEqualTo: userId)
.addSnapshotListener { querySnapshot, error in
// Make sure we have a document
guard let document = querySnapshot?.documents.first else {
print("Error fetching document: \(error!)")
return
}
// We have a document and it has data. Use it.
self.balance = document.get("balance") as! Double
dispatchGroup.leave() /// 3
}
}
dispatchGroup.notify(queue: .main) { /// 4
//update here
// Format the balance
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
let balanceString = currencyFormatter.string(from: self.balance as NSNumber)
self.balanceLabel.setTitle(balanceString, for: .normal)
}

Related

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

How to fetch Firestore document that contains a reference to another document in Swift 5?

I am trying to fetch data from a Firebase Database in Swift 5.
I have two collections: "users" and "locations" (locations has a reference to user).
I have attached a snapshot listener to the locations collection so I will fetch data every time. there is a change and add it to a global array.
Therefore after I fetch the location document I want to also fetch the user that is referencing to.
I had to add a DispatchGroup so it will wait for all users to get fetched and added to the array before passing the array through the completion block.
However, it is adding duplicates of each location into an array. How should I fix this problem? Thanks in advance for your help :).
static func startListener(completion: #escaping ((_ data: [Location]) -> Void)){
db.collection(LOCATIONS_DOCUMENT).addSnapshotListener {
(snap,error) in
let dispatchGroup = DispatchGroup()
if(error != nil){
print(error!)
}
else{
self.locations.removeAll()
for location in snap!.documents{
dispatchGroup.enter()
let map = location.data()
let id = location.documentID
let image = map["image"] as! String
let title = map["title"] as! String
let latitude = map["latitude"] as! NSNumber
let longitude = map["longitude"] as! NSNumber
let userRef = map["user"] as! DocumentReference
let user = User();
userRef.getDocument { (document, error) in
if(error == nil){
let userMap = document!.data()
let userId = document!.documentID
let userFirstName = userMap!["firstName"] as! String
user.id = userId
user.firstName = userFirstName
}
let newLocation = Location(id: id, image: image, title: title, latitude: Double(truncating: latitude), longitude: Double(truncating: longitude), user: user)
self.locations.append(newLocation)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(locations)
}
}
}

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.

Firestore Listener Document Changes

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 ...)

Weird Inconsistent tableview Swift 4

I'm downloading images stored in firebase and put them in an array.
But when I'm retrieving them and display in tableview, the images seems to be out of order and weirdly inconsistent every time. Anyone know how the code can be fixed?
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
retrieveData()
retrieveImage()
}
func retrieveImage(){
let userID = Auth.auth().currentUser?.uid
ref.child("Images").observeSingleEvent(of: .value, with: { (snapshot) in
let userImage = snapshot.value as? NSDictionary
let imageURLArray = userImage?.allKeys
if userImage != nil{
for index in 0...userImage!.count-1{
let imageProfile = userImage![imageURLArray?[index]] as? NSDictionary
let imageURL = imageProfile!["url"]
let usernameDB = imageProfile!["username"]
let timeCreatedDB = imageProfile!["timeCreated"] as? Double
let date = NSDate(timeIntervalSince1970: timeCreatedDB!)
let dayTimePeriodFormatter = DateFormatter()
dayTimePeriodFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateString = dayTimePeriodFormatter.string(from: date as Date)
let storageRef = Storage.storage().reference(forURL: imageURL as! String)
self.usernames.insert(usernameDB as! String, at: 0)
self.timesCreated.insert(dateString, at: 0)
storageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error.localizedDescription)
} else {
let image = UIImage(data: data!)
self.images.insert(image!, at: 0)
self.tableView.reloadData()
}
}
}
}
}) { (error) in
print(error.localizedDescription)
}
}
The problem is that the call to get the image inside the loop returns the image asynchronously and so while you may request all of the images in a specific order, they're not guaranteed to return in that order mostly because of varying file sizes (the smaller images will likely return sooner). You're also reloading the table after each image get, which isn't contributing to your problem, but a design I would recommend against; simply load the table once after all of the data is in hand.
To fix your problem, you should use a dispatch group to notify you when all of the images have been downloaded asynchronously; then you can sort the array and load the table. This is a common place to use dispatch groups--inside loops that contain async calls. Declare the dispatch group outside of the loop, enter the group before each async call, and leave that group after each async return. Then the dispatch group calls its completion block, where you sort the array and load the table.
func retrieveImage(){
let userID = Auth.auth().currentUser?.uid
ref.child("Images").observeSingleEvent(of: .value, with: { (snapshot) in
let userImage = snapshot.value as? NSDictionary
let imageURLArray = userImage?.allKeys
if userImage != nil{
let dispatchGroup = DispatchGroup() // create dispatch group outside of the loop
for index in 0...userImage!.count-1{
let imageProfile = userImage![imageURLArray?[index]] as? NSDictionary
let imageURL = imageProfile!["url"]
let usernameDB = imageProfile!["username"]
let timeCreatedDB = imageProfile!["timeCreated"] as? Double
let date = NSDate(timeIntervalSince1970: timeCreatedDB!)
let dayTimePeriodFormatter = DateFormatter()
dayTimePeriodFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateString = dayTimePeriodFormatter.string(from: date as Date)
let storageRef = Storage.storage().reference(forURL: imageURL as! String)
self.usernames.insert(usernameDB as! String, at: 0)
self.timesCreated.insert(dateString, at: 0)
dispatchGroup.enter() // enter this group before async call
storageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error.localizedDescription)
} else {
let image = UIImage(data: data!)
self.images.insert(image!, at: 0)
//self.tableView.reloadData() don't reload here
}
dispatchGroup.leave() // leave this group after async return
}
}
// this is called after all of the async calls in the loop returned
// it puts you on the main thread
dispatchGroup.notify(queue: .main, execute: {
self.tableView.reloadData() // load table
})
}
}) { (error) in
print(error.localizedDescription)
}
}
The code above does not include the sorting mechanism because that's a bit more coding than I wanted to do but the execution is simple. To keep the images in the same order, you can do a number of things, one of which is to enumerate the loop, take the count of each loop iteration and attach it to the image, and then sort the array by that number before you load the table.
for (count, image) in images.enumerated() {
// take the count and attach it to each image
// the easiest way I think is to create a custom object
// that contains the image and the count
}
// and then in your dispatch group completion handler...
dispatchGroup.notify(queue: .main, execute: {
// sort the images by their enumerated count before loading the table
imagesArray.sort { $0.thatCount < $1.thatCount }
DispatchQueue.main.async {
self.tableView.reloadData() // load the table
}
})