I am trying to build methods with completion blocks for nested requests. The issue is that a completion block catches to early for parent requests (meaning that the child requests haven't actually completed yet). So far I haven't found a way for a child request to communicate back to the parent request other than what I've done in my example below (which is to count the amount of child requests have completed and compare it against the expected amount of requests).
The example below is working against a Firestore database. Imagine a user has multiple card games (decks) with each multiple cards. I'm grateful for any help how to build better completion blocks for cases like these:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var decksCount = Int()
var cardsCount = Int()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
decksCount += 1
cardsCount += cardSnapshot.count
if decksCount == deckSnapshot.count {
completion(cardsCount)
}
}
})
})
}
}
}
Here is the solution, using DispatchGroup, found with #meggar's help in the comments:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var cardsCount = Int()
let group = DispatchGroup()
group.enter()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
group.enter()
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
cardsCount += cardSnapshot.count
}
group.leave()
})
})
}
group.leave()
}
group.notify(queue: .main) {
completion(cardsCount)
}
}
Related
I'm doing my very first IOS app using Cloud Firestore and have to make the same queries to my database repeatedly. I would like to get rid of the duplicate lines of code. This is examples of func where documents ID are duplicated. Also I using other queries as .delete(), .addSnapshotListener(), .setData(). Should I refactor all that queries somehow or leave them because they were used just for one time?
#objc func updateUI() {
inputTranslate.text = ""
inputTranslate.backgroundColor = UIColor.clear
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { [self] (document, error) in
if let document = document, document.exists {
let document = document
let label = document.data()?.keys.randomElement()!
self.someNewWord.text = label
// Fit the label into screen
self.someNewWord.adjustsFontSizeToFitWidth = true
self.checkButton.isHidden = false
self.inputTranslate.isHidden = false
self.deleteBtn.isHidden = false
} else {
self.checkButton.isHidden = true
self.inputTranslate.isHidden = true
self.deleteBtn.isHidden = true
self.someNewWord.adjustsFontSizeToFitWidth = true
self.someNewWord.text = "Add your first word to translate"
updateUI()
}
}
}
#IBAction func checkButton(_ sender: UIButton) {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, error) in
let document = document
let label = self.someNewWord.text!
let currentTranslate = document!.get(label) as? String
let translateField = self.inputTranslate.text!.lowercased().trimmingCharacters(in: .whitespaces)
if translateField == currentTranslate {
self.inputTranslate.backgroundColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
updateUI()}
} else {
self.inputTranslate.backgroundColor = UIColor.red
self.inputTranslate.shakingAndRedBg()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
self.inputTranslate.text = ""
}
}
}
}
func deletCurrentWord () {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, err) in
let document = document
if let err = err {
print("Error getting documents: \(err)")
} else {
let array = document!.data()
let counter = array!.count
if counter == 1 {
// The whole document will deleted together with a last word in list.
let user = Auth.auth().currentUser?.email
self.db.collection(K.FStore.collectionName).document(user!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
self.updateUI()
}
}
} else {
// A current word will be deleted
let user = Auth.auth().currentUser?.email
let wordForDelete = self.someNewWord.text!
self.db.collection(K.FStore.collectionName).document(user!).updateData([
wordForDelete: FieldValue.delete()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
self.updateUI()
}
}
}
}
}
}
Another query example
func loadMessages() {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.data(){
for item in snapshotDocuments {
if let key = item.key as? String, let translate = item.value as? String {
let newMessage = Message(key: key, value: translate)
self.messages.append(newMessage)
}
}
DispatchQueue.main.async {
self.messages.sort(by: {$0.value > $1.value})
self.secondTableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.secondTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
enum Error {
case invalidUser
case noDocumentFound
}
func fetchDocument(onError: #escaping (Error) -> (), completion: #escaping (FIRQueryDocument) -> ()) {
guard let user = Auth.auth().currentUser?.email else {
onError(.invalidUser)
return
}
db.collection(K.FStore.collectionName).document(user).getDocument { (document, error) in
if let error = error {
onError(.noDocumentFound)
} else {
completion(document)
}
}
}
func updateUI() {
fetchDocument { [weak self] error in
self?.hideShowViews(shouldHide: true, newWordText: nil)
} completion: { [weak self] document in
guard document.exists else {
self?.hideShowViews(shouldHide: true, newWordText: nil)
return
}
self?.hideShowViews(shouldHide: false, newWordText: document.data()?.keys.randomElement())
}
}
private func hideShowViews(shouldHide: Bool, newWordText: String?) {
checkButton.isHidden = shouldHide
inputTranslate.isHidden = shouldHide
deleteBtn.isHidden = shouldHide
someNewWord.adjustsFontSizeToFitWidth = true
someNewWord.text = newWordText ?? "Add your first word to translate"
}
The updateUI method can easily be refactored using a simple guard statement and then taking out the common code into a separate function. I also used [weak self] so that no memory leaks or retain cycles occur.
Now, you can follow the similar approach for rest of the methods.
Use guard let instead of if let to avoid nesting.
Use [weak self] for async calls to avoid memory leaks.
Take out the common code into a separate method and use a Bool flag to hide/show views.
Update for step 3:
You can create methods similar to async APIs for getDocument() or delete() etc and on completion you can update UI or perform any action. You can also create a separate class and move the fetchDocument() and other similar methods in there and use them.
Im trying to download data from Firestore, append it to one array to be sorted and then append it to another array once all the data has been sorted. I have a dispatchgroup which manages all the data is being downloaded before moving on however I don't know how to or where to append the sorted array ( temparray1 / temparray2)to the master array (closeunisSameCourse/nearbyUnis). I have tried using dispatch.wait to wait for both temp arrays to be appended but it just hangs.
func nearbyUnisSameCourse(completion: #escaping (_ success: Bool) -> Void) {
DispatchQueue.main.async {
self.spinner.startAnimating()
}
self.dispatchGroup.enter()
service.loadUniversityAndCourse { (uni, course) in
defer{ self.dispatchGroup.leave() }
let nearbyUnis = ClosestUnis()
let closeUniArray = nearbyUnis.getClosestUnis(University: uni)
for uni in closeUniArray {
let UniRef = Firestore.firestore().collection("User-Universities").document(uni)
self.dispatchGroup.enter()
UniRef.getDocument { (snapshot, error) in
defer{ self.dispatchGroup.leave() }
if let error = error{
print(error.localizedDescription)
}
else {
//append their data to an array
guard let data = snapshot?.data() else {return}
let stringArray = Array(data.keys)
for user in stringArray {
self.dispatchGroup.enter()
let usersRef = Firestore.firestore().collection("users").document(user)
usersRef.getDocument { (snapshot, error) in
defer{ self.dispatchGroup.leave() }
if let error = error {
print(error.localizedDescription)
}
else {
let data = snapshot?.data()
if let dictionary = data as [String:AnyObject]? {
let Info = UserInfo(dictionary: dictionary)
if Info.Course == course {
print(Info.username!)
self.tempArray1.append(Info)
self.tempArray1.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) > Double(time2.Created!.seconds)
}
self.closeunisSameCourse.append(contentsOf: self.tempArray1)
self.tempArray1.removeAll()
}
else {
self.tempArray2.append(Info)
print(Info.username!)
self.tempArray2.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) > Double(time2.Created!.seconds)
}
}
}
}
}
//end of for user loop
}
//outside user for loop
print("now appending")
self.nearbyUnis.append(contentsOf: self.tempArray2)
print(self.nearbyUnis.description)
self.tempArray2.removeAll()
}}}}
self.dispatchGroup.notify(queue: .main) {
print("Finished")
//self.spinner.stopAnimating()
//print(self.nearbyUnis.description)
self.tableView.reloadData()
completion(true)
}
}
}
fixed by sorting by name
else {
self.nearbyUnis.append(Info)
print(Info.username!)
self.nearbyUnis.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) >
Double(time2.Created!.seconds)}
self.nearbyUnis.sort { (uni1, uni2) -> Bool in
return uni2.University! > uni1.University!}
}
I am working on a similar feature to 'liking/unliking a post'.
I have an MVVM architecture as;
struct MyStructModel {
var isLiked: Bool? = false
}
class MyStructView {
var isLiked: Bool
init(myStructModel: MyStructModel) {
self.isLiked = myStructModel.isLiked ?? false
}
}
I successfully get the value of whether the post is liked or not here;
func isPostLiked(documentID: String, completion: #escaping (Bool) -> Void) {
guard let authID = auth.id else { return }
let query = reference(to: .users).document(authID).collection("liked").document(documentID)
query.getDocument { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let data = snapshot?.data() else { return }
if let value = data["isLiked"] as? Bool {
completion(value)
} else {
completion(false)
}
}
}
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let snapshotDocuments = snapshot?.documents else { return }
for document in snapshotDocuments {
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
// isLiked is nil here...
self.isPostLiked(documentID: post.documentID!) { (isLiked) in
post.isLiked = isLiked
print("MODEL SAYS: \(post.isLiked!)")
// isLiked is correct value here...
}
posts.append(post)
}
completion(posts)
}
}
}
However, when it gets to my cell the value is still nil.
Adding Cell Code:
var post: MyStructView? {
didSet {
guard let post = post else { return }
print(post.isLiked!)
}
}
Your isLiked property is likely nil in your cells because the retrieveReviews function doesn't wait for the isPostLiked function to complete before completing itself.
You could easily solve this issue by using DispatchGroups. This would allow you to make sure all of your Posts have their isLiked value properly set before being inserted in the array, and then simply use the DispatchGroup's notify block to return all the loaded posts via the completion handler:
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { [weak self] (snapshot, error) in
guard let self = self else { return }
if error != nil {
return
}
guard let documents = snapshot?.documents else { return }
let dispatchGroup = DispatchGroup()
for document in documents {
dispatchGroup.enter()
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
self.isPostLiked(documentID: post.documentID!) { isLiked in
post.isLiked = isLiked
posts.append(post)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(posts)
}
}
}
I am using the google maps distance matrix api and using a compeletion handler to pass my async function calls like this:
func configureRoute(origin:String,destination:String, completionHandler: #escaping (_ duration:Int) -> ()){
let jsonURL = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=place_id:\(origin)&destinations=place_id:\(destination)&key=MYAPI"
guard let url = URL(string: jsonURL ) else {return}
print(jsonURL)
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
do {
let route = try JSONDecoder().decode(Welcome.self, from: data)
// print(self.durations)
completionHandler(route.rows[0].elements[0].duration.value)
}
catch let jsonErr {
}
let dataAsString = String(data: data, encoding: .utf8)
}.resume()
}
and then I try and use that result like so, however my output results are still blank and I am unable to use any of the calls that I have received. I am not that good with compeltion handlers, so if anyone could let me know what I have done wrong?
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
}
}
groupedDurations.append(durations)
durations.removeAll()
}
print(groupedDurations)
}
After using Dispatch group this is my updated code:
func majorConfigure(){
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
let dispatchGroup = DispatchGroup()
for eachArray in finalRoutes{
for i in 0..<(eachArray.count-1){
dispatchGroup.enter()
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
print(self.groupedDurations)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main){
self.groupedDurations.append(self.durations)
self.durations.removeAll()
}
}
}
The result when I print groupedDurations is:
[]
[]
[]
[]
[]
[]
give this a try :
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
if i == eachArray.count - 1{
groupedDurations.append(durations)
}
if i == eachArray.count - 1 && eachArray == finalRoutes.last! {
print(groupedDurations)
durations.removeAll()
}
}
}
}
}
EDITED ANSWER
You can achieve this in this manner :
var indexFinalRoutes = 0
var indexEachArray = 0
func loopFinalRoutes(){
self.indexEachArray = 0
let eachArray = finalRoutes[indexFinalRoutes]
loopEachArray(eachArray: eachArray) { (success) in
self.indexFinalRoutes = self.indexFinalRoutes + 1
if self.indexFinalRoutes < self.finalRoutes.count - 1{
self.loopFinalRoutes()
}else{
self.indexFinalRoutes = 0
print(self.groupedDurations)
}
}
}
func loopEachArray(eachArray : [String] ,
completion: #escaping (_ success:Bool) -> Void){
if indexEachArray + 1 < eachArray.count - 1 {
configureRoute(origin: eachArray[indexEachArray], destination: eachArray[indexEachArray+1]){
duration in
self.durations.append(duration)
self.indexEachArray = self.indexEachArray + 1
let nextEachArray = self.finalRoutes[self.indexFinalRoutes]
self.loopEachArray(eachArray: nextEachArray, completion: completion)
}
}else{
groupedDurations.append(durations)
durations.removeAll()
completion(true)
}
}
call loopFinalRoutes functuin from your majorConfigure function and you will have the output.
I have a method that calls three functions that each make a request to Firebase and pass back data in a completion handler. Once the data from 2 of the completion handlers is sent back, I call another method to pass in the data and send back a valid result. Is nesting blocks like this a sign of poor design?
func fetchNearbyUsers(for user: User, completionHandler: usersCompletionHandler?) {
self.fetchAllUsers(completionHandler: { (users: [User]) in
ChatProvider.sharedInstance.fetchAllChatrooms(completionHandler: { (chatrooms: [Chatroom]) in
self.validateNewUsers(currentUser: user, users: users, chatrooms: chatrooms, completionHandler: { (validUsers: [User]) in
guard validUsers.isEmpty == false else {
completionHandler?([])
return
}
completionHandler?(validUsers)
})
})
})
}
// Other Methods
func fetchAllUsers(completionHandler: usersCompletionHandler?) {
var users: [User] = []
let group = DispatchGroup()
let serialQueue = DispatchQueue(label: "serialQueue")
DatabaseProvider.sharedInstance.usersDatabaseReference.observe(.value, with: {
snapshot in
guard let data = snapshot.value as? [String: AnyObject] else { return }
for (_, value) in data {
guard let values = value as? [String: AnyObject] else { return }
guard let newUser = User(data: values) else { return }
group.enter()
newUser.fetchProfileImage(completionHandler: { (image) in
serialQueue.async {
newUser.profileImage = image
users.append(newUser)
group.leave()
}
})
}
group.notify(queue: .main, execute: {
completionHandler?(users)
})
})
}
func fetchAllChatrooms (completionHandler: chatroomsHandler?) {
var chatrooms = [Chatroom]()
let group = DispatchGroup()
let serialQueue = DispatchQueue(label: "serialQueue")
DatabaseProvider.sharedInstance.chatroomsDatabaseReference.observe(.value, with: { snapshot in
guard let data = snapshot.value as? [String: AnyObject] else { return }
for (_, value) in data {
guard let value = value as? [String: AnyObject] else { return }
guard let newChatroom = Chatroom(data: value) else { return }
group.enter()
self.fetchChatroomUsers(chatroom: newChatroom, completionHandler: { (chatroom) in
serialQueue.async {
chatrooms.append(newChatroom)
group.leave()
}
})
}
group.notify(queue: .main, execute: {
completionHandler?(Array(Set<Chatroom>(chatrooms)))
})
})
}
private func validateNewUsers(currentUser: User, users: [User], chatrooms: [Chatroom], completionHandler: usersCompletionHandler?) {
let chatPartners = self.chatPartners(currentUser: currentUser, chatrooms: chatrooms)
let validUsers = users
.filter { currentUser.id != $0.id }
.filter { currentUser.distanceFromUser(atLocation: $0.coordinates) <= 8046.72 }
.filter { !chatPartners.contains($0.id) }
completionHandler?(validUsers)
}
private func chatPartners (currentUser: User, chatrooms: [Chatroom]) -> Set<String> {
var results = [String]()
for chatroom in chatrooms {
if currentUser.id == chatroom.firstUserId {
results.append(chatroom.secondUserId)
} else if currentUser.id == chatroom.secondUserId {
results.append(chatroom.firstUserId)
}
}
return Set(results)
}