How do I asynchronously initialize firebase firestore listeners while also knowing when all of the tasks are done? - swift

Basically, at the launch of my app, I want to load the latest data from firebase from about 4-5 different documents. Then I also want to set up a listener to monitor data changes. I do this by calling 4-5 similar functions that take a dispatchGroup as an argument. I may be approaching this completely wrong but I could not think of any other way to do it. I just want to load those documents, set up listeners, and take certain action whenever those docs are loaded at the launch of the app.
// app launch
let dispatch = DispatchGroup()
getFirebaseDocument1(dispatch: dispatch)
getFirebaseDocument2(dispatch: dispatch)
getFirebaseDocument3(dispatch: dispatch)
getFirebaseDocument4(dispatch: dispatch)
getFirebaseDocument5(dispatch: dispatch)
dispatch.notify(queue:main) {
// execute some code to execute after all the documents are fetched
}
// typical getFirebaseDocument code
dispatch.enter()
let ref = someFirestoreReference
ref.addSnapshotListener { (snapshot, error) in
if let error = error{
// handle error
} else {
// load the document
}
dispatch.leave()
}
The code works fine when it's launched but crashes whenever the listener receives an update. I know this is because the dispatch.leave() is called in the listener function. However, I cannot seem to figure out a clever solution to where I can asynchronously load the data from firebase at launch while also setting up listeners. I would also prefer not to nest closures within one another as it wouldn't be asynchronous and it would also be a pain.

I may be wrong, but you should leave the group inside your block, here is the example how I do it using group
class func getRates(completion: #escaping EmptyBlock) {
let eurRequest = APIConfigs.request(part: "rs/price/history")
let usdRequest = APIConfigs.request(part: "rs/price/history/usd")
let group = DispatchGroup()
group.enter()
sendRequest(request: eurRequest, method: .get, parameters: ServerParameters.rates()) { response in
Course.current.addRate(rates: ratesRequest(response: response), type: .eur)
group.leave()
}
group.enter()
sendRequest(request: usdRequest, method: .get, parameters: ServerParameters.rates()) { response in
Course.current.addRate(rates: ratesRequest(response: response), type: .usd)
group.leave()
}
group.notify(queue: .main) {
completion()
}
}

Related

How to observe multiple Firebase entries in a loop and run completion block once every observe function has finished?

I'm trying to create a feed that should include posts made by people a user follows, and I'm using following function to get posts for user feed:
func observePosts(for userID: String, completion: #escaping ([UserPost], String) -> Void) {
let databaseKey = "Posts/\(userID)/"
databaseRef.child(databaseKey).queryOrderedByKey().queryLimited(toLast: 12).observe(.value) { snapshot in
var retrievedPosts = [UserPost]()
var lastSeenPostKey = ""
let dispatchGroup = DispatchGroup()
for snapshotChild in snapshot.children.reversed() {
guard let postSnapshot = snapshotChild as? DataSnapshot,
let postDictionary = postSnapshot.value as? [String: Any]
else {
print("Couldn't cast snapshot as DataSnapshot")
return
}
dispatchGroup.enter()
lastSeenPostKey = postSnapshot.key
do {
let jsonData = try JSONSerialization.data(withJSONObject: postDictionary as Any)
let decodedPost = try JSONDecoder().decode(UserPost.self, from: jsonData)
UserService.shared.observeUser(for: decodedPost.userID) { postAuthor in
decodedPost.author = postAuthor
retrievedPosts.append(decodedPost)
dispatchGroup.leave()
}
} catch {
print("Couldn't create UserPost from postDictionary")
}
}
dispatchGroup.notify(queue: .main) {
completion(retrievedPosts, lastSeenPostKey)
}
}
}
First step I'm getting user feed posts with .observe function and after that I'm iterating through every received post calling a UserService.shared.observeUser method that observes user data for every post to set post author's profile picture and username.
The problem with my method is that I'm using DispatchGroup's .enter and .leave methods to notify once every observe function got user data so that I could call completion block in which I'm reloading tableView.
Sure it works fine first time but since it's observe function it's called every time there's a change to user so once user updates his data (profile pic, usernama etc.) it crashes on dispatchGroup.leave() line since it's not balanced with dispatchGroup.enter()
What would be a correct approach to solve this issue?
I've been trying to solve this problem for 3 days searching all the internet, and only advice I found is to fetch it one time using observeSingleValue() instead of observing, but I really need to observe user profile data to keep profile pics and other necessary data up to date.

Control when a different number of requests are completed in Swift

I have the following code that runs about :
for resFoto in resFotosResenhaEscolhidas {
jsonRequestUploadImagem = ResFotosModel.createJsonFotoResenha(resFoto)
let requestUploadImagem: NSMutableURLRequest = serviceRepository.clientURLRequest(wsUploadImagem, typeAutho: .basic, parms: "", body: jsonRequestUploadImagem as Dictionary<String, AnyObject>)
serviceRepository.post(requestUploadImagem, retryLogin: true, completion: {isOk,msgError,httpCode,needLogin,response in
self.checkResponseUploadImagemFotoResenha(response as AnyObject, httpCode)
})
}
func checkResponseUploadImagemFotoResenha(_ response:AnyObject, _ httpCode:Int) {
if httpCode != 200 {
let string = String(data: response as! Data, encoding: .utf8)
print( string!+" \n Erro HTTP: "+String(httpCode) )
} else {
// httpCode == 200
let data: Data = response as! Data // received from a network request, for example
let jsonResponse = try? JSONSerialization.jsonObject(with: data, options: [])
print("json response upload foto")
print(jsonResponse!)
}
}
The serviceRepository.post just run a "urlSession.dataTask", but I wanna know how can I control when the completion of the request.
The "resFotosResenhaEscolhidas" object contains 0 to 4 array inside it depending on the call. So, the code runs and create from 0 to 4 requests.
If the 4 requests are running, I just wanna know how can I check when they are finished?
Look at using a DispatchGroup. You'd create a DispatchGroup when you get ready to begin making network calls.
You'd call enter() on your dispatch group each time you begin a new URLSession task (or other async task.) You'd call leave() on the dispatch group in the completion handler for each task.
After you've submitted your async tasks, you'd call the dispatch group's notify() method to submit a block that will execute once all your async tasks are complete. (It's important that you wait until you've submitted your async tasks before calling notify(). If you try to call it before submitting your tasks, it invokes it's closure immediately since no tasks are running.)
I wrote a little demo project that uses a DispatchGroup to monitor a set of async tasks. (In the demo the tasks just delay for a random time before completing, and generate a random number.)
It waits until they've all completed, and then indicates the task that returned the largest value.
You can check it out on Github: DispatchGroupDemo on github

How does the semaphore keep async loop in order?

I've set up this script to loop through a bunch of data in the background and I've successfully set up a semaphore to keep everything (the array that will populate the table) in order but I cannot exactly understand how or why the semaphore keeps the array in order. The dispatchGroup is entered, the loop stops and waits until the image is downloaded, once the image is gotten the dispatchSemaphore is set to 1 and immediately the dispatchGroup is exited and the semaphore set back to 0. The semaphore is toggled so fast from 0 to 1 that I don't understand how it keeps the array in order.
let dispatchQueue = DispatchQueue(label: "someTask")
let dispatchGroup = DispatchGroup()
let dispatchSemaphore = DispatchSemaphore(value: 0)
dispatchQueue.async {
for doc in snapshot.documents {
// create data object for array
dispatchGroup.enter()
// get image with asynchronous completion handler
Storage.storage().reference(forURL: imageId).getData(maxSize: 1048576, completion: { (data, error) in
defer {
dispatchSemaphore.signal()
dispatchGroup.leave()
}
if let imageData = data,
error == nil {
// add image to data object
// append to array
}
})
dispatchSemaphore.wait()
}
// do some extra stuff in background after loop is done
}
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
The solution is in your comment get image with asynchronous completion handler. Without the semaphore all image downloads would be started at the same time and race for completion, so the image that downloads fastest would be added to the array first.
So after you start your download you immediately wait on your semaphore. This will block until it is signaled in the callback closure from the getData method. Only then the loop can continue to the next document and download it. This way you download one file after another and block the current thread while the downloads are running.
Using a serial queue is not an option here, since this would only cause the downloads to start serially, but you can’t affect the order in which they finish.
This is a rather inefficient though. Your network layer probably can run faster if you give it multiple requests at the same time (think of parallel downloads and HTTP pipelining). Also you're 'wasting' a thread which could do some different work in the meantime. If there is more work to do at the same time GCD will spawn another thread which wastes memory and other resources.
A better pattern would be to skip the semaphore, let the downloads run in parallel and store the image directly at the correct index in your array. This of course means you have to prepare an array of the appropriate size beforehand, and you have to think of a placeholder for missing or failed images. Optionals would do the trick nicely:
var images: [UIImage?] = Array(repeating: nil, count: snapshot.documents.count)
for (index, doc) in snapshot.documents.enumerated() {
// create data object for array
dispatchGroup.enter()
// get image with asynchronous completion handler
Storage.storage().reference(forURL: imageId).getData(maxSize: 1048576) { data, error in
defer {
dispatchGroup.leave()
}
if let imageData = data,
error == nil {
// add image to data object
images[index] = image
}
}
}
The DispatchGroup isn't really doing anything here. You have mutual exclusion granted by the DispatchSemaphor, and the ordering is simply provided by the iteration order of snapshot.documents

Swift loadItem closure not running

I am writing a share extension, but my closure that would capture and save the shared attachment is not running. How can I find out why? The switch branch executes, the attachment is there. There is no error message, it just never runs.
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents {
let fType = attachment.registeredTypeIdentifiers[0]
if attachment.hasItemConformingToTypeIdentifier(fType) {
switch fType {
case kUTTypeImage as String as String:
do {
attachment.loadItem(forTypeIdentifier: fType, options: nil, completionHandler: { data, error in
print("AppImage")
let url = data as! URL
if let imageData = try? Data(contentsOf: url) {
self.appImage = UIImage(data: imageData)
self.saveImage(image: self.appImage!)
}
})
} // public image case
Once completeRequestReturningItems (CRRI) is executed, your completion handlers for loadItem will no longer be called (they are effectively canceled at that point). Therefore you must synchronize your asynchronous tasks to ensure that you don't execute CRRI until your completion handlers have finished or until you no longer care. From your comments, it sounds like you are invoking loadItem and immediately proceeding to call CRRI.
See answers to this related question: iOS 8 Share extension loadItemForTypeIdentifier:options:completionHandler: completion closure not executing
I prefer the answer there that uses a dispatch group.

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