Working with Nested Async Firebase Calls Swiftui

Working with nested async Firebase calls SwiftUI

You need to re-enter a the group again when doing the second getDocuments call. As it will also be asynchron. Something like this should do the trick:

let myGroup = DispatchGroup()
//Enter first time for first async call
myGroup.enter()
self.errorMessage = ""
let usersRef = self.db.collection("Users").document("Users").collection("Users")
if self.test == false {
self.errorMessage = "test failed"
} else{
usersRef.getDocuments {(snap, err) in //Starting first async call

for document in snap!.documents{
print("loop")
let user = document["username"] as! String
let userRef = usersRef.document(user)

//Enter second time for second async call
myGroup.enter()
userRef.getDocument { (snapshot, err) in // starting second async call
if err != nil {
print(err)
} else {
let self.error = snapshot!["error"] as! Bool
if self.error == true{
self.errorMessage = "error"
print("error")
}
print("what3")
}
print("what2")
//Leave second async call
myGroup.leave()
}
print("what1")
}
//Leave first async call
myGroup.leave()
print("what4")
}

myGroup.notify(queue: DispatchQueue.global(qos: .background)) {
print("HERE I SHOULD BE DONE")
}

print("what5")
}

}

Recommendation: When using DispatchGroup/ Asynchron calls try to divide them. For e.g. use a function for each call as it can get really quickly really messy. Having them separated and then combining them in one method makes it also easier to modify and or find errors.

Working With Async Firebase Calls SwiftUI

This explains the DispatchGroup() a little bit.

You just have one litte mistake in your code then it should be working.
Make sure to enter() the group outside of the Firebase getDocuments() call. As this already makes the request and takes time thus the process will continue.

This little simple example should help you understand it:

func dispatchGroupExample() {

// Initialize the DispatchGroup
let group = DispatchGroup()

print("starting")

// Enter the group outside of the getDocuments call
group.enter()

let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in

if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
}

// leave the group when done
group.leave()
}

// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
}
}

When waiting for multiple asynchronous calls use completing in the asynchronous function which you let return as soon as you leave the group. Full eg. below:

class Test {

init() {
self.twoNestedAsync()
}

func twoNestedAsync() {
let group = DispatchGroup() // Init DispatchGroup

// First Enter
group.enter()
print("calling first asynch")

self.dispatchGroupExample() { isSucceeded in

// Only leave when dispatchGroup returns the escaping bool

if isSucceeded {
group.leave()
} else {
// returned false
group.leave()
}
}

// Enter second
group.enter()
print("calling second asynch")

self.waitAndReturn(){ isSucceeded in

// Only return once the escaping bool comes back
if isSucceeded {
group.leave()
} else {
//returned false
group.leave()
}

}

group.notify(queue: .main) {
print("all asynch done")
}
}

// Now added escaping bool which gets returned when done
func dispatchGroupExample(completing: @escaping (Bool) -> Void) {

// Initialize the DispatchGroup
let group = DispatchGroup()

print("starting")

// Enter the group outside of the getDocuments call
group.enter()

let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in

if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}

// leave the group when succesful and done
group.leave()
}

if let error = error {
// make sure to handle this
completing(false)
group.leave()
}
}

// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")

//send escaping bool.
completing(true)
}
}

func waitAndReturn(completing: @escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
print("Done waiting for 2 seconds")
completing(true)
})
}
}

This gives us the following output:

DispatchGroup with completing and escaping output

SwiftUI not updating variable after async firebase call

SwiftUI views automatically trigger updates according to the state of their views. Because the Firebase calls are asynchronous, you cannot expect that a synchronous call to the fetch function will return with the correct result.

Instead, you need to update a Published value (on the main thread) that will then broadcast an update message to all subscribers of that value. SwiftUI automatically handles the subscribe mechanism with StateObject or ObservedObject and triggers a new request for the body of the view.

To get your code working with SwiftUI, adjust checkFriendRequest as follows:

Change this:

            if pendingFriendRequests[key] == true {
self.isFriend = 1
} else if pendingFriendRequests[key] == false {
self.isFriend = 2
}

To: (Because it triggers UI events and all UI must run on main thread)

          DispatchQueue.main.async {
if pendingFriendRequests[key] == true {
self.isFriend = 1
} else if pendingFriendRequests[key] == false {
self.isFriend = 2
}
}

In your view, change the VStack to:

      VStack {
if userData.isFriend == 1 {
//button for if returned 1
} else if userData.isFriend == 2 {
//button for if returned 2
} else {

}
}.onAppear() {
userData.checkFriendRequest(otherUserUID: otherUserUID)
}

Using dispatch queues with multiple async calls

When the second async call depends on the first, but no other first data calls from any other objects within ref, then this should work:

func Foo(completion: ()->()) {
let dispatchGroup = DispatchGroup()

for doc in documents {
dispatchGroup.enter()
ref.getDocument(completion: { (snapshot, error) in
userCountRef.getDocuments(completion: { (snap, error) in
let Object = Object(init with snapshot data)
dispatchGroup.leave()
}
}
}

// .main here is the main DispatchQueue. Can be any queue desired
dispatchGroup.notify(.main) {
completion()
}
}

This notify will ensure that all the enter's have been matched by a closing leave before calling your completion. See more at the apple docs here.

Firebase cloud function ends before return

The problem is the last line here:

return userRef
.doc(data.userId)
.collection("locationIds")
.where("status", "==", true)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
return locationRef...

Since you're looping over the documents in querySnapshot, you have multiple calls return locationRef... in there, and you have no code that ensures that all those reads have finished before the Cloud Function is terminated.

Whenever you need to wait for multiple operations on the same level in your code, your answer is in using Promise.all:

return userRef
.doc(data.userId)
.collection("locationIds")
.where("status", "==", true)
.get()
.then((querySnapshot) => {
return Promise.all(querySnapshot.docs.map((doc) => {
return locationRef...

So the changes:

  • We return a Promise.all() which only resolves once all nested reads are done.

  • We use querySnapshot.docs.map instead of querySnapshot.forEach, so that we get an array of promises to pass to Promise.all.

Wait until swift for loop with asynchronous network requests finishes executing

You can use dispatch groups to fire an asynchronous callback when all your requests finish.

Here's an example using dispatch groups to execute a callback asynchronously when multiple networking requests have all finished.

override func viewDidLoad() {
super.viewDidLoad()

let myGroup = DispatchGroup()

for i in 0 ..< 5 {
myGroup.enter()

Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
myGroup.leave()
}
}

myGroup.notify(queue: .main) {
print("Finished all requests.")
}
}

Output

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

SWIFTUI Firebase Retrieving Subcollection Data

This is the issue

func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()

fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})

return fetchedTasks
}

This is an asynchronous function as well (see the closure) and you have to give Firebase time to retrieve the data from the server and handle it within the Firebase closure.

What's happening here is that while you are doing that within the Firebase closure itself, that's not happening within this closure. return fetchedTasks is returning before fetchedTasks = tasks.

I would call the firebase function directly since it doesn't appear you need the middleman retrieveAllTasks function

self.fsGetTasks(documentID: "some_doc", completion: { taskArray in
for task in taskArray {
print(task
}
})

If you do, you need to add an @escaping clause to that as well and not use return fetchedTasks

Why are nested tasks not canceled when they parent task is cancelled?

You asked:

Why are nested tasks not canceled when they parent task is cancelled?

Because you are using Task, which is for unstructured concurrency. As the docs say, it is not a subtask, but a new “top-level task”.

If you want to enjoy the benefits of structured concurrency (e.g., automatic propagation of cancelation), use task group instead of Task { ... }. E.g.:

let observationTask = Task {
await withTaskGroup(of: Void.self) { group in
group.addTask {
...
}
group.addTask {
...
}
}
}

For more information, see WWDC 2021 video Explore structured concurrency in Swift. Or see the discussion of structured vs unstructured concurrency in The Swift Programming Language: Concurrency.


Please note that the above assumes that these subtasks (and possibly updateTableView, too) are cancelable. The fact that we do not see try anywhere suggests it has not implemented cancelation support (where you would Task.checkCancellation or test Task.isCancelled and manual handle the exiting of the loops if canceled).


To illustrate cancelation, consider:

import os.log

let log = OSLog(subsystem: "Test", category: .pointsOfInterest)

class Demonstration {
private var observationTask: Task<Void, Error>?

func start() {
observationTask = Task {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { [self] in
for i in 0 ..< 4 {
try await process(i)
}
}
group.addTask { [self] in
for i in 200 ..< 204 {
try await process(i)
}
}
try await group.waitForAll()
}
}
}

func stop() {
os_signpost(.event, log: log, name: #function)

observationTask?.cancel()
}

func process(_ value: Int) async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "%d", value)
defer { os_signpost(.end, log: log, name: #function, signpostID: id) }

try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC) // simulate something slow and asynchronous
}
}

If I do not cancel the observationTask and profile it in Instruments’s “Points of Interest” tool, I see:

Sample Image

But if I do cancel it (at the Ⓢ signpost), I see the following:

Sample Image

That works because:

  • We used structured concurrency withThrowingTaskGroup; and
  • The subtasks were cancelable.


Related Topics



Leave a reply



Submit