Swift / How to use dispatch_group with multiple called web service?
I am using dispatch_group
to call Firebase requesting functions and get notified once the request is complete to be able to work with the result then. In this scenario I've just put a print statement.
func loadStuff() {
dispatch_group_enter(group)
myFirebaseFunction() {
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("done")
}
}
func myFirebaseFunction(completionHandler: () -> ()) {
let usersRef = firebase.child("likes")
usersRef.observeEventType(.Value, withBlock: { snapshot in
if snapshot.exists() {
let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])
for item in sorted {
dict.append(item as! NSDictionary)
}
}
completionHandler()
})
}
This code is working fine. The problem is that, during runtime, data will be added into the Firebase Database. That is why I have to use observeEventType
instead of observeSingleEventOfType
.
This means there is an observer during runtime and in case data has been added to the database, the block within myFirebaseFunction
will be called again.
Once this happens, the app crashes because dispatch_group_leave(group)
has been called without dispatch_group_enter(group)
. As long as I got this right.
dispatch_group_enter(group)
myFirebaseFunction() {
dispatch_group_leave(group) // crash here
}
If I change it to observeSingleEventOfType
, no crash occurs, but new added Data to Firebase will not be observed.
What's the best practice to use dispatch_group
with multiple run Web Services? Or what do I have to do to fix my issue? Help is very appreciated.
PS Currently I'm using Swift 2.3, but an upgrade to Swift 3 is planned, so it would be very awesome to receive an answer capable for both.
The Problem
As you stated, calls to dispatch_group_enter
and dispatch_group_leave
must be balanced. Here, you are unable to balance them because the function that performs the actual real-time fetching only calls leave.
Method 1 - All calls on the group
If you take no issue with myFirebaseFunction
always performing its work on that dispatch group, then you could put both enter and leave inside there, perhaps with a beginHandler and completionHandler:
func loadStuff() {
myFirebaseFunction(beginHandler: {
dispatch_group_enter(group)
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("done")
}
}, completionHandler: { dispatch_group_leave(group) })
}
func myFirebaseFunction(beginHandler: () -> (), completionHandler: () -> ()) {
let usersRef = firebase.child("likes")
usersRef.observeEventType(.Value, withBlock: { snapshot in
beginHandler()
if snapshot.exists() {
let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])
for item in sorted {
dict.append(item as! NSDictionary)
}
}
completionHandler()
})
}
Here, the completion handler would still be set to dispatch_group_leave
by loadStuff
, but there is also a begin handler that would call dispatch_group_enter
and also dispatch_group_notify
. The reason notify would need to be called in begin is that we need to ensure that we have already entered the group before we call notify or the notify block will execute immediately if the group is empty.
The block you pass to dispatch_group_notify
will only be called exactly once, even if blocks are performed on the group after notify has been called. Because of this, it might be safe for each automatic call to observeEventType
to happen on the group. Then anytime outside of these functions that you need to wait for a load to finish, you can just call notify.
Edit: Because notify is called each time beginHandler
is called, this method would actually result in the notify block being called every time, so it might not be an ideal choice.
Method 2 - Only first call on group, several methods
If what you really need is for only the first call of observeEventType
to use the group, then one option is to have two versions of myFirebaseFunction
: one much like the one you already have and one using observeSingleEventOfType
. Then load stuff could call both of those functions, only passing dispatch_group_leave
as a completion to one of them:
func loadStuff() {
dispatch_group_enter(group)
myInitialFirebaseFunction() {
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("done")
}
myFirebaseFunction({})
}
func myInitialFirebaseFunction(completionHandler: () -> ()) {
let usersRef = firebase.child("likes")
usersRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
processSnapshot(snapshot)
completionHandler()
})
}
func myFirebaseFunction(completionHandler: () -> ()) {
let usersRef = firebase.child("likes")
usersRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
processSnapshot(snapshot)
completionHandler()
})
}
func processSnapshot(snapshot: FDataSnapshot) {
if snapshot.exists() {
let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])
for item in sorted {
dict.append(item as! NSDictionary)
}
}
}
Method 3 - Only first call on group, no extra methods
Note that because loadStuff
in "Method 2" basically loads things from Firebase twice, it might not be as efficient as you'd like. In that case you could instead use a Bool
to determine whether leave should be called:
var shouldLeaveGroupOnProcess = false
func loadStuff() {
dispatch_group_enter(group)
shouldLeaveGroupOnProcess = true
myFirebaseFunction() {
if shouldLeaveGroupOnProcess {
shouldLeaveGroupOnProcess = false
dispatch_group_leave(group)
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("done")
}
}
func myFirebaseFunction(completionHandler: () -> ()) {
let usersRef = firebase.child("likes")
usersRef.observeEventType(.Value, withBlock: { snapshot in
if snapshot.exists() {
let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])
for item in sorted {
dict.append(item as! NSDictionary)
}
}
completionHandler()
})
}
Here, even if multiple calls to observeEventType
are made during the initial load, leave
is guaranteed to be called only once and no crashed should occur.
Swift 3
PS Currently I'm using Swift 2.3, but an upgrade to Swift 3 is planned, so it would be very awesome to receive an answer capable for both.
Dispatch has gotten a complete overhaul in Swift 3 (it is object-oriented), so code that works well on both is not really a thing :)
But the concepts of each of the three methods above are the same. In Swift 3:
- Create your group with one of the inits of
DispatchGroup
-
dispatch_group_enter
is now the instance methodenter
on the group -
dispatch_group_leave
is now the instance methodleave
on the group -
dispatch_group_notify
is now the instance methodnotify
on the group