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:
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 ofquerySnapshot.forEach
, so that we get an array of promises to pass toPromise.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:
But if I do cancel
it (at the Ⓢ signpost), I see the following:
That works because:
- We used structured concurrency
withThrowingTaskGroup
; and - The subtasks were cancelable.
Related Topics
In Swift, Dynamic Height for UItextview in UIcollectionview
Using Scenekit for Hittesting Not Returning a Hit with Scnnode
[Bool]' to 'Nil' Always Returns True - Issue in Swiftui
Realitykit - Add Force to Entity at Specific Point
Error When Running Coreml in The Background: Error Computing Nn Outputs Error
Urlsession Datatask Method Returns 0 Bytes of Data
Swift Janus Can Not Publish Video, But Get Remote Video Successful - Can Not Know Reason
Modify Scpreferences Persistent Storage: Invalid Argument
How to Modify Codable Class Properties
Firebase Swift 3 Get List of Child in a Array
Xcode 9.3 Watchkit Crash on Wkinterfacebutton Tap
How to Pass a String Containing Markdown to Custom View
How to Get The .Mlmodel to Be Used in Swift Playground
F# Asynchronous Http Request - Parse JSON Response
Swift - Scanning with Ikscannerdeviceview on Osx