So I am trying to restructure my API calls to make use of dispatchGroups so I don't have to make collectionViews and other elements reload so quick. I know that with dispatch groups you typically have to create one, enter, and then leave a certain number of times. Upon completion of this, you typically notify the main queue to do some operation. However, in the code snippet below it notifies the main queue before I even enter the dispatch group once. Which is throwing things way off. If anyone could look at my code and tell me what's going wrong I would really appreciate that.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
print("entered dispatch group")
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
dispatchGroup1.leave()
print("left dispatch group")
}
})
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
I am running dispatch groups in other places. Im not sure if that would affect anything but I just felt like it was important to note in this question.
You need to enter the group before you start the asynchronous task and leave in the completion of the asynchronous task. You aren't executing the first enter until the first asynchronous task completes, so when execution hits your notify the dispatch group is empty and it fires straight away.
It is also important that you call leave the same number of times that you call enter or the group will never empty, so be wary of calling leave inside conditional statements.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
disatchGroup1.enter()
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
print("left dispatch group")
}
dispatchGroup1.leave()
})
dispatchGroup1.leave()
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
Related
Currently making an app in which a view controller takes in the addressString and then geocodes the address to retrieve the latitude and longitude of said address as seen below inside of a #objc method.
#objc func addLocationTouchUpInside() {
let addressString = "\(streetAddressField.text!), \(cityField.text!), \(stateField.text!)"
var location = Location(id: Location.setid(), locationName: nil, addressString: addressString, latitude: nil, longitude: nil, HoHid: nil)
// This geocoder instance is a let property initialized with the class
geocoder.geocodeAddressString(location.addressString) { placemark, error in // Completion handler starts here...
print("WHY")
if error != nil {
print("error: \(error!)")
}
if let placemark = placemark?[0] {
if let addresslocation = placemark.location {
location.latitude = addresslocation.coordinate.latitude
location.longitude = addresslocation.coordinate.longitude
}
}
print("3: \(location.latitude)")
}
do {
try AppDatabase.shared.dbwriter.write({ db in
try location.insert(db)
})
self.currentUser?.locationid = location.id
try AppDatabase.shared.dbwriter.write({ db in
if var user = try User.fetchOne(db, key: self.currentUser?.id) {
user = self.currentUser!
try user.update(db)
}
})
} catch {
fatalError("\(error)")
}
let locationInputViewController = LocationInputViewController()
locationInputViewController.setUser(user: self.currentUser!)
locationInputViewController.setLocation(location: location)
locationInputViewController.setHoH()
if let navigationController = self.navigationController {
navigationController.pushViewController(locationInputViewController, animated: true)
}
}
The issue I am having is that any code within the completion handler (of type CLGeocodeCompletionHandler) does not seem to execute. Originally I had assumed that location simply wasnt being mutated for some reason. I then learned that nothing within the completion handler executes.
This question may seem like a duplicate to this post however it seems a clear answer was not quite found. Via some comments on said post I had set both set a custom location for the simulator and used my personal iphone and saw no difference.
EDIT: I have updated the question with the entire function as requested in the comments. Second the completion handler I have been referring to would be that of CLGeocodeCompletionHandler. Defined as
typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, Error?) -> Void
where
func geocodeAddressString(_ addressString: String, completionHandler: #escaping CLGeocodeCompletionHandler)
in the documentation. I have also commented the line where the completion handler starts.
Finally in only ONE of my runs was this strange error outputted:
#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero
Swift closures strongly capture reference types.
DispatchGroup is a reference type.
My questions have to do with the following code:
func getUsername(onDone: #escaping (_ possUsername: String?) -> ())
{
//Post request for username that calls onDone(retrievedUsername)...
}
func getBirthdate(using username: String?, onDone: #escaping (_ possBday: String?) -> ())
{
//Post request for token that calls onDone(retrievedToken)...
}
func asyncTasksInOrder(onDone: #escaping (_ resultBDay: String?) -> ())
{
let thread = DispatchQueue(label: "my thread", qos: .userInteractive, attributes: [],
autoreleaseFrequency: .workItem, target: nil)
thread.async { [weak self, onDone] in
guard let self = self else {
onDone(nil)
return
}
let dg = DispatchGroup() //This is a reference type
var retrievedUsername: String?
var retrievedBday: String?
//Get username async first
dg.enter()
self.getUsername(onDone: {[weak dg](possUsername) in
retrievedUsername = possUsername
dg?.leave() //DG is weak here
})
dg.wait()
//Now that we've waited for the username, get bday async now
dg.enter()
self.getBirthdate(using: retrievedUsername, onDone: {[weak dg](possBday) in
retrievedBday = possBday
dg?.leave() //DG is also weak here
})
dg.wait()
//We've waited for everything, so now call the return callback
onDone(retrievedBday)
}
}
So the two closures inside of asyncTasksInOrder(onDone:) each capture dg, my DispatchGroup.
Is it even necessary to capture my dispatch group?
If I don't capture it, how would I even know I've got a retain cycle?
What if the dispatch group evaporates during one of the callback executions? Would it even evaporate since it's waiting?
Is it unnecessarily expensive to instantiate a DispatchQueue like this often (disregarding the .userInteractive)? I'm asking this particular question because spinning up threads in Android is extremely expensive (so expensive that JetBrains has dedicated lots of resources towards Kotlin coroutines).
How does dg.notify(...) play into all of this? Why even have a notify method when dg.wait() does the same thing?
I feel like my understanding of GCD is not bulletproof, so I'm asking to build some confidence. Please critique as well if there's anything to critique. The help is truly appreciated.
1) No, the dispatch group is captured implicitly. You don't even need to capture self in async because GCD closures don't cause retain cycles.
2) There is no retain cycle.
3) Actually you are misusing DispatchGroup to force an asynchronous task to become synchronous.
4) No, GCD is pretty lightweight.
5) The main purpose of DispatchGroup is to notify when all asynchronous tasks – for example in a repeat loop – are completed regardless of the order.
A better solution is to nest the asynchronous tasks. With only two tasks the pyramid of doom is manageable.
func asyncTasksInOrder(onDone: #escaping (String?) -> Void)
{
let thread = DispatchQueue(label: "my thread", qos: .userInteractive, autoreleaseFrequency: .workItem)
thread.async {
//Get username async first
self.getUsername { [weak self] possUsername in
guard let self = self else { onDone(nil); return }
//Now get bday async
self.getBirthdate(using: possUsername) { possBday in
//Now call the return callback
onDone(possBday)
}
}
}
}
I have a (custom, linked-list based) queue that I want to deserialize when the app starts and serialize when the app stops, like so (AppDelegate.swift):
func applicationWillResignActive(_ application: UIApplication) {
RequestManager.shared.serializeAndPersistQueue()
}
func applicationDidBecomeActive(_ application: UIApplication) {
RequestManager.shared.deserializeStoredQueue()
}
The issue is during serialization when I exit the app. Here's the code that's running:
public func serializeAndPersistQueue() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(queue) // Bad access here
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
}
catch {
print(error)
}
}
As you can see, fairly straightforward. It uses the JSONEncoder to convert my queue to a data object, then writes that data to the file at url.
However, during encoder.encode() I get EXC_BAD_ACCESS every time, without fail.
Additionally, I should note that peak and dequeue operations are conducted on the queue from a background thread. I'm not sure if that makes a difference due to my lack of understanding surrounding GCD. Here's what that method looks like:
private func processRequests() {
DispatchQueue.global(qos: .background).async { [unowned self] in
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)
while !self.queue.isEmpty {
group.enter()
let request = self.queue.peek()!
self.sendRequest(request: request, completion: { [weak self] in
_ = self?.queue.dequeue()
semaphore.signal()
group.leave()
})
semaphore.wait()
}
group.notify(queue: .global(), execute: { [weak self] in
print("Ending the group")
})
}
}
Lastly, I'll note that:
My queue conforms to the Codable protocol just fine––well, there are no compiler errors, at least. If its implementation beyond that matters, let me know and I'll show it.
The crash occurs a few seconds after I exit the app, while the execution of the processRequests function stops immediately after
I have DispatchWorkItem that fetch some info and I have a button that displays the info. The worker looks like this:
worker = DispatchWorkItem {
let info = getInfo()
DispatchQueue.main.async {
self.setInfo(info)
}
}
on viewDidLoad I add the worker to the global queue
DispatchQueue.global().asyncAfter(deadline: .now() + 0.35, execute: worker!)
When the user clicks on the button I need the execution to be blocked until the worker has finished execution.
#IBAction func readInfo(_ sender: UIButton) {
// WAIT UNTIL THE WORKER HAS FINISHED EXECUTION
...
I managed to do so by having the worker looking like this:
worker = DispatchWorkItem {
let info = getInfo()
self.setInfo(info)
}
and by checking if the info was set every 200 ms after the user clicks on the button:
#IBAction func readInfo(_ sender: UIButton) {
while(info == nil){
usleep(2000)
}
...
However I want all the variables to be accessible only by the main thread.
To be honest, I don't clearly understand what you wanted, but it's inappropriate to use such a thing as usleep(2000).
So, there is a possible solution, but it's general and you probably need to modify it for your needs.
let group = DispatchGroup()
var info: Info?
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.35) { [weak self] in
self?.getInfo()
}
}
func getInfo() {
group.enter()
asyncFunc()
group.notify(queue: .main) { [weak self] in
self?.setInfo(self?.info)
}
}
func asyncFunc() {
..... { [weak self] info in
self?.info = info
self?.group.leave()
}
}
If you want to disable user interaction while something is loading, it's better to show progressive loader, but not just freeze the application. In case with the loader, users will understand, that the app isn't frozen.
There is an example of using DispatchWorkItem with DispatchGroup:
let dispatchWorkItem = DispatchWorkItem{
print("work item start")
sleep(1)
print("work item end")
}
let dg = DispatchGroup()
//submiy work items to the group
let dispatchQueue = DispatchQueue(label: "custom dq")
dispatchQueue.async(group: dg) {
print("block start")
sleep(2)
print("block end")
}
DispatchQueue.global().async(group: dg, execute: dispatchWorkItem)
//print message when all blocks in the group finish
dg.notify(queue: DispatchQueue.global()) {
print("dispatch group over")
}
Code from here
I've been messing around with dispatch groups and am wondering how the placement of the notify callback of a dispatch group affects when the callback will be called. I'm reading data from my database and then fetching a picture for each item in the data that was read. When I have the notify outside of the initial database read block I notice it gets called immediately, however when the notify is inside the block it behaves the proper way. Here is my code:
override func viewDidAppear(_ animated: Bool) {
let ref = FIRDatabase.database().reference().child("users").child((FIRAuth.auth()?.currentUser?.uid)!).child("invites")
ref.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in snapshots {
self.dispatchGroup.enter()
let info = petInfo(petName: child.value! as! String, dbName: child.key)
print(info)
self.invitesData[info] = UIImage()
let picRef = FIRStorage.storage().reference().child("profile_images").child(info.dbName+".png")
picRef.data(withMaxSize: 1024 * 1024) { (data, error) -> Void in
if error != nil {
print(error?.localizedDescription ?? "Error getting picture")
}
// Create a UIImage, add it to the array
self.invitesData[info] = UIImage(data: data!)!
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
print("YOOO")
self.invitesArray = Array(self.invitesData.keys)
print(self.invitesArray)
self.inviteTable.reloadData()
})
}
})
}
This code behaves properly when the notify is within the original database read block. However if I place this after the ref.observeSingleEvent block the notify gets called immediately.
Can somebody please explain this to me?
Yes. Asynchronous code :-)
Code execution runs all the way through to the end of the function, and then the completion handler will be called