Executing function calls sequentially - swift

I am trying to fetch user input everytime the user edits the textfield and search users according to the input. In the beginning of the search function, I remove the users array and perform the search. However, the function is called concurrently and it empties the array even before performing the search. This might result in duplicate data in the user array. Is there a way to serialize the whole function call? I looked through GCD documents and tried to implemented some methods, but it didn't work out.
This is the source code
// handles editting movement of the search textfield
#objc func textIsChanging(){
//need to serialize execution to avoid concurrency.
// wait until previous call completes?
searchUsers()
}
// search user from database.
private func searchUsers(){
// empty user array and reload table view
removeUsers()
if let searchText = searchField.text?.lowercased() {
// search with starting index..
Api.User.queryUsersWithStartText(withText: searchText) { (user) in
if(CurrentUserInfo.uid != user.uid){
self.users.append(user)
self.tableView.reloadData()
}
}
}
}

You can use a DispatchGroup to execute asynchronous functions sequentially. You need to ensure that the enter() and leave() calls on the DispatchGroup are balanced and if you do so, the code in dispatchGroup.notify(queue: DispatchQueue.main) {...} will only execute if there is currently block in the DispatchGroup and hence another block can safely be added. By calling searchUsers() from inside dispatchGroup.notify, you ensure that the function is only called once its previous call finished execution.
let dispatchGroup = DispatchGroup()
#objc func textIsChanging(){
dispatchGroup.notify(queue: DispatchQueue.main) {
searchUsers()
}
}
// search user from database.
private func searchUsers(){
dispatchGroup.enter()
// empty user array and reload table view
removeUsers()
if let searchText = searchField.text?.lowercased() {
// search with starting index..
Api.User.queryUsersWithStartText(withText: searchText) { (user) in
if(CurrentUserInfo.uid != user.uid){
self.users.append(user)
self.tableView.reloadData()
}
dispatchGroup.leave()
}
} else {
dispatchGroup.leave()
}
}

Related

Why is My Code running this way and is There a better way to solve this issue

// In the code below I am trying to return an array from data in firestore, the array always returned empty when I put the handler outside the for loop so I had to use an if statement inside the for loop to get the array containing the data. after using the print statement you see in the code i found out that the compiler is going over the entire function before entering the for loop, (print("5") & (print("6") are the first to run and when I put the handler outside the for it will also be triggered and return an empty array
**
func getMyGames(joinedGamesIDs: [String], handler: #escaping(_ games: [GameViewModal]) -> ()) {
var games = [GameViewModal]()
if !joinedGamesIDs.isEmpty{
for id in joinedGamesIDs {
db.collection("games").document(id).getDocument { (document, error) in
if let document = document, document.exists {
if let game = self.getGameViewModal(document: document){
games.append(game)
print("1")
print(games.count)
}
print("2")
print(games.count)
}
print("3")
print(games.count)
if games.count == (joinedGamesIDs.count){
handler(games)
}
print("4")
print(games.count)
}
}
print("5")
print(games.count)
}
print("6")
print(games.count)
}
**
I've embedded my explanations in the code commentary for easier reading. But the problem you have is that you aren't coordinating these async tasks (the getting of each document). You must coordinate them so when the last one finishes, you can "return" the array from the function. This function doesn't technically "return" anything (except Void) but the completion handler, in a way, "returns" the array which is why I put it in quotes. These semantic details matter and it helps to understand everything better.
func getMyGames(joinedGamesIDs: [String], handler: #escaping (_ games: [GameViewModel]) -> ()) {
guard !joinedGamesIDs.isEmpty else {
// If there is nothing to do, always consider
// calling the handler anyway, with an empty
// array, so the caller isn't left hanging.
return handler([])
}
// Set up a Dispatch Group to coordinate the multiple
// async tasks. Instatiate outside of the loop.
let group = DispatchGroup()
var games: [GameViewModel] = []
for id in joinedGamesIDs {
// Enter the group on each iteration of async work
// to be performed.
group.enter()
db.collection("games").document(id).getDocument { (document, error) in
if let doc = document,
doc.exists,
let game = self.getGameViewModal(document: doc) {
games.append(game)
} else if let error = error {
// Always print errors when in development.
print(error)
}
// No matter what happens inside the iteration,
// whether there was a success in getting the
// document or a failure, always leave the group.
group.leave()
}
}
// Once all of the calls to enter the group are equalled
// by the calls to leave the group, this block is called,
// which is the group's own completion handler. Here is
// where you ultimately call the function's handler and
// return the array.
group.notify(queue: .main) {
handler(games)
}
}

addDocument completion handler is never called

I have a function adding documents to a collection in firebase. It is done using a for loop. I have a DispatchGroup and I am calling enter() at the start of each iteration of the loop. After each document has been added I want to call the completion handler of the addDocument method. In the completion handler I want to call leave() on my DispatchGroup, so that I eventually can perform a segue when all documents have been added. My problem is that the completion handler never seems to get called as the messages never get printed. I can see that the documents get added to my collection in firebase every time I run the code. Have I misunderstood something or is there something wrong with my approach? Any help would be very appreciated. A simplified example of my code looks something like this:
func uploadDocumentToFirebase(names: String[])
{
for name in names
{
dispatchGroup.enter()
collection.addDocument(data: ["name": name], completion: {error in
print("Document: \(name) was uploaded to firebase")
self.dispatchGroup.leave()
})
}
}
The actual documents I'm adding have 6 fields instead of the 1 shown in my example, if that makes any difference.
There are many ways to do this - here's two. First is using a dispatch group and the second is using and index technique.
I have an array of words and want to write them to Firestore, notifying as each one is written and then when they are all written.
let arrayOfWords = ["boundless", "delicious", "use", "two", "describe", "hilarious"]
Here's the dispatch group code. We enter the group, write the data and in the completion handler, when done, leave. When all have been left group.notify is called.
func writeWordUsingDispatchGroup() {
let group = DispatchGroup()
let wordCollection = self.db.collection("word_collection")
for word in self.arrayOfWords {
group.enter()
let dataToWrite = ["word": word]
wordCollection.addDocument(data: dataToWrite, completion: { error in
print("\(word) written")
group.leave()
})
}
group.notify(queue: .main) {
print("all words written")
}
}
And then the index code. All this does is calculates the index of the last object in the array and then iterates over the array enumerated (so we get the index). Then when the index of the current loop matches the last index, we know we're done.
func writeWordsUsingIndex() {
let wordCollection = self.db.collection("word_collection")
let lastIndex = self.arrayOfWords.count - 1
for (index, word) in self.arrayOfWords.enumerated() {
let dataToWrite = ["word": word]
wordCollection.addDocument(data: dataToWrite, completion: { error in
print("\(word) written")
if index == lastIndex {
print("all words written")
}
})
}
}
Edit:
Maybe you can run a completion handler so that your exits are in the same place as your group? I generally write completion handlers in situations like this this and call them where you have self.dispatchGroup.leave(). You can put self.dispatchGroup.leave() in the completion block which might help? It seems like your group has an uneven number of entry points and exit points. Organizing with a completion block might help find it?
completion: (#escaping (Bool) -> ()) = { (arg) in })
Original:
Would you mind using this setData code instead of addDcoument to see if it helps? You can add your dispatch to this code and see if it all works. If not I will keep thinking it through...
Also maybe check to make sure the input array isn't empty (just print it to console in the method).
let db = Firestore.firestore()
db.collection("your path").document("\(your document name)").setData([
"aName": "\(name)",
"anEmail": "\(email)",
]) { err in
if let _ = err {
print("Error writing document:")
} else {
print("Document successfully written!")
}
}
My problem is that the completion handler never seems to get called as
the messages never get printed.
It seems you're not calling completion inside your addDocument method, at least for the case, when a document is successfully added.

Is there a better way to optimise this Firestore Query?

Right now I query Firestore arrays based on another array of strings. This works fine but I would like to know if this is correct and if there is a way to optimise it to make it faster.
Here is my code.
var exampleArray = [test]()
func loadData(textArray : [String]){
let db = Firestore.firestore()
for i in 0..<textArray.count{
db.collection("testCollection").whereField("testField", arrayContains: textArray[i]).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
let res = querySnapshot!.documents.compactMap({test(dictionary: $0.data())})
self.exampleArray.append(contentsOf: res)
self.summaryTableView.reloadData()
SVProgressHUD.dismiss()
print(self.textArray[i])
}
}
}
}
}
Thanks in advance.
UPDATE:
Some info:
Right now the VisionCloudRecognition process and query take about 10-11sec. 7 sec the recognition and 3-4 the query.
The Database has 7700 documents, 7 fields each.
Basically my app does the following:
User takes a photo
With OCR the image becomes text and i append the text in an array of strings.We don't know how many strings are going to be. Maybe 0 or maybe 50.
Then i query Firestore arrays based on the array of strings.
As pointed out in the comments, you're running a bunch of async requests in a loop (that's always bad) and you're reloading the data as each one resolves causing the tableView to keep repainting with new data.
What you want to do is add all of those db requests to a DispatchGroup. As they resolve, add the data to your array. Once they have all finished, do a single tableView.reloadData() and dismiss your SVProgressHud.
This should not only speed things up because multiple calls can happen in parallel, but your user will not have table rows bouncing around as new data is added and the tableView reloads X number of times in rapid fire.
EDIT: You asked for an example - this is very quick and written here on SO, so look for XCode to notify you of any typos or errors.
class MyVC: UIViewController {
var exampleArray = [test]()
let db = Firestore.firestore()
let dispatchGroup = DispatchGroup()
override func viewDidLoad(animated: bool) {
super.viewDidLoad(animated)
myTableView.delegate = self
myTableView.dataSource = self
for i in 0..<textArray.count {
getTestFieldRecords(i)
}
dispatchGroup.notify(queue: .main) {
self.myTableView.reloadData()
}
}
private func getTestFieldRecords(_ record: Int) {
dispatchGroup.enter()
db.collection("testCollection").whereField("testField", arrayContains: textArray[i]).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
// rest of your code because I'm too lazy to format it
}
self.dispatchGroup.leave()
}
}
}
Make sure you call leave() every time - error or not. The DispatchGroup will not finish until all enter() commands are finished with a leave() command. Hope this helps point you in the right direction!

Swift: Control dependent functions, avoid callback hell

How can I design the following code better? I have the feeling that the code can lead to a callback hell. Every function depends on completion of the previous one.
Current Solution (bad):
#objc func restoreDocuments(UID: UID) {
DispatchQueue.global(qos: .background).async {
//1. Load user details from RemoteServer#1
UserManager.RemoteServer.loadUserFromRemoteServer(userUID: UID) { (userDict) in
//2. After user is loaded save user to local database
UserManager.LocalDB.saveUser(userData: userDict, completion: {
//After User is restored, restore his documents from RemoteServer#2 (IDs provided in userDetails)
let userDocumentsArray = getDocumentIDsFromUser(userUID: UID)
//Loop through array to get every ID
for ID in userDocumentsArray{
//load each document by ID
loadDocumentsRemote(documentID: ID) { (document) in
//Save loaded document
saveDocumentsLocal(document, completion: {
//At the end populate the UI with the restored documents
DispatchQueue.main.async {
populateUI()
}
})
})
}
})
}
}
I would imagine something like the following code. But I don't know how to communicate the different steps among each other. So that task 2 does not start before task 1 is finished.
What I imagine (simplified):
#objc func restoreDocuments(UID: UID) {
//1. Restore User
UserManager.RemoteServer.loadUser(UID){ (user) in }
UserManager.LocalDB.saveUser(user)
// -> WHEN FINISH PROCCED TO STEP 2 🚨
//2. Load Documents
UserManager.LocalDB.getDocumentIDsFromUser( { (IdArray) in
for ID in IdArray {
RemoteServer.DocManager.loadDocument(ID) { (retrievedDocument) in
LocalDB.DocManager.saveDocument(retrievedDocument)
}
}
}
// -> WHEN FINISH PROCCED TO STEP 3 🚨
//3. Finish
DispatchQueue.main.async {
populateUI()
}
}
But how do I do that? And is that a good approach at all 🤔?
Take a look at futures and promises, two related design patterns that address this issue very well. My company uses BrightFutures, a third party library that offers a decent implementation of both.
You can start by extracting the closures into variables:
let onDocumentsSaved: () -> Void = {
DispatchQueue.main.async {
populateUI()
}
}
let onDocumentsLoaded: (Document) -> Void { document in
saveDocumentsLocal(document, completion: onDocumentsSaved)
}
// continue with other closures, in reverse order of execution
That will clear up your indentation and the steps will be clearly visible. If you want to wait for multiple steps (e.g. multiple documents) in one step, you can use DispatchGroup.
Every step can be also easily extracted into a function, or, you can make your class to work as a state machine.
Also, it's a good idea to group connected methods into utility methods, e.g. your load and save can be grouped to one method with a completion handler, for example:
func loadUserAndSave(userId, completion)
func loadDocumentsAndSave(userId, completion)
then your method could be simplified to (pseudocode):
loadUserAndSave(userId) {
loadDocumentsAndSave {
DispatchQueue.main.async {
populateUI()
}
}
}
Which again would be much simpler .

Swift closure async order of execution

In my model have function to fetch data which expects completion handler as parameter:
func fetchMostRecent(completion: (sortedSections: [TableItem]) -> ()) {
self.addressBook.loadContacts({
(contacts: [APContact]?, error: NSError?) in
// 1
if let unwrappedContacts = contacts {
for contact in unwrappedContacts {
// handle constacts
...
self.mostRecent.append(...)
}
}
// 2
completion(sortedSections: self.mostRecent)
})
}
It's calling another function which does asynchronous loading of contacts, to which I'm forwarding my completion
The call of fetchMostRecent with completion looks like this:
model.fetchMostRecent({(sortedSections: [TableItem]) in
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.state = State.Loaded(sortedSections)
self.tableView.reloadData()
}
})
This sometimes it works, but very often the order of execution is not the way as I would expect. Problem is, that sometimes completion() under // 2 is executed before scope of if under // 1 was finished.
Why is that? How can I ensure that execution of // 2 is started after // 1?
A couple of observations:
It will always execute what's at 1 before 2. The only way you'd get the behavior you describe is if you're doing something else inside that for loop that is, itself, asynchronous. And if that were the case, you'd use a dispatch group to solve that (or refactor the code to handle the asynchronous pattern). But without seeing what's in that for loop, it's hard to comment further. The code in the question, alone, should not manifest the problem you describe. It's got to be something else.
Unrelated, you should note that it's a little dangerous to be updating model objects inside your asynchronously executing for loop (assuming it is running on a background thread). It's much safer to update a local variable, and then pass that back via the completion handler, and let the caller take care of dispatching both the model update and the UI updates to the main queue.
In comments, you mention that in the for loop you're doing something asynchronous, and something that must be completed before the completionHandler is called. So you'd use a dispatch group to do ensure this happens only after all the asynchronous tasks are done.
Note, since you're doing something asynchronous inside the for loop, not only do you need to use a dispatch group to trigger the completion of these asynchronous tasks, but you probably also need to create your own synchronization queue (you shouldn't be mutating an array from multiple threads). So, you might create a queue for this.
Pulling this all together, you end up with something like:
func fetchMostRecent(completionHandler: ([TableItem]?) -> ()) {
addressBook.loadContacts { contacts, error in
var sections = [TableItem]()
let group = dispatch_group_create()
let syncQueue = dispatch_queue_create("com.domain.app.sections", nil)
if let unwrappedContacts = contacts {
for contact in unwrappedContacts {
dispatch_group_enter(group)
self.someAsynchronousMethod {
// handle contacts
dispatch_async(syncQueue) {
let something = ...
sections.append(something)
dispatch_group_leave(group)
}
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
self.mostRecent = sections
completionHandler(sections)
}
} else {
completionHandler(nil)
}
}
}
And
model.fetchMostRecent { sortedSections in
guard let sortedSections = sortedSections else {
// handle failure however appropriate for your app
return
}
// update some UI
self.state = State.Loaded(sortedSections)
self.tableView.reloadData()
}
Or, in Swift 3:
func fetchMostRecent(completionHandler: #escaping ([TableItem]?) -> ()) {
addressBook.loadContacts { contacts, error in
var sections = [TableItem]()
let group = DispatchGroup()
let syncQueue = DispatchQueue(label: "com.domain.app.sections")
if let unwrappedContacts = contacts {
for contact in unwrappedContacts {
group.enter()
self.someAsynchronousMethod {
// handle contacts
syncQueue.async {
let something = ...
sections.append(something)
group.leave()
}
}
}
group.notify(queue: .main) {
self.mostRecent = sections
completionHandler(sections)
}
} else {
completionHandler(nil)
}
}
}