swift 3 DispatchGroup leave causes crash when called in helper class function - swift

I'm using DispatchGroup.enter() and leave() to process a helper class's reverseG async function. Problem is clear, I'm using mainViewController's object to call mainViewControllers's dispatchGroup.leave() in helper class! Is there a way to do it?
Same code works when reverseG is declared in the main view controller.
class Geo {
var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, _ completion: #escaping (CLPlacemark) -> ()) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { (placemarks, error) in
if let error = error {
print("error: \(error.localizedDescription)")
}
if let placemarks = placemarks, placemarks.count > 0 {
let placemark = placemarks.first!
completion(placemark) // set ViewController's properties
} else {
print("no data")
}
obj.dispatchGroup.leave() // ** ERROR **
}
}
}
Function call from main view controller
dispatchGroup.enter()
Geo.reverseG(coordinates, setValues) // completionHandler: setValues
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}

Every leave call must have an associated enter call. If you call leave without having first called enter, it will crash. The issue here is that you're calling enter on some group, but reverseG is calling leave on some other instance of ViewController. I'd suggest passing the DispatchGroup as a parameter to your reverseG method. Or, better, reverseG shouldn't leave the group, but rather put the leave call inside the completion handler that reserveG calls.
dispatchGroup.enter()
Geo.reverseG(coordinates) { placemark in
defer { dispatchGroup.leave() }
guard let placemark = placemark else { return }
// use placemark here, e.g. call `setValues` or whatever
}
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
And
class Geo {
// var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { placemarks, error in
if let error = error {
print("error: \(error.localizedDescription)")
}
completion(placemarks?.first)
// obj.dispatchGroup.leave() // ** ERROR **
}
}
}
This keeps the DispatchGroup logic at one level of the app, keeping your classes less tightly coupled (e.g. the Geo coder doesn't need to know whether the view controller uses dispatch groups or not).
Frankly, I'm not clear why you're using dispatch group at all if there's only one call. Usually you'd put whatever you call inside the completion handler, simplifying the code further. You generally only use groups if you're doing a whole series of calls. (Perhaps you've just simplified your code snippet whereas you're really doing multiple calls. In that case, a dispatch group might make sense. But then again, you shouldn't be doing concurrent geocode requests, suggesting a completely different pattern, altogether.

Passed dispatchGroup as parameter with function call and it worked.
Geo.reverseG(coordinates, dispatchGroup, setValues)

my two cents to show how can work:
(maybe useful for others..)
// Created by ing.conti on 02/02/21.
//
import Foundation
print("Hello, World!")
let r = AsyncRunner()
r.runMultiple(args: ["Sam", "Sarah", "Tom"])
class AsyncRunner{
static let shared = AsyncRunner()
let dispatchQueue = DispatchQueue(label: "MyQueue", qos:.userInitiated)
let dispatchGroup = DispatchGroup.init()
func runMultiple(args: [String]){
let count = args.count
for i in 0..<count {
dispatchQueue.async(group: dispatchGroup) { [unowned self] in
dispatchGroup.enter()
self.fakeTask(arg: args[i])
}
}
_ = dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
func fakeTask(arg: String){
for i in 0..<3 {
print(arg, i)
sleep(1)
}
dispatchGroup.leave()
}
}

Related

How to synchronize two asynchronous functions

I have a delegate which contains the following function:
func didFailToConnect(_ interface) {
}
and also i have an asynchronous call:
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error != nil {
}
if let placemarks = placemarks {
}
}
what i want to achieve is to see if the 2 calls has been finished and inform the user with an alert.Which is the best way to implement this i'm thinking of using DispatchGroup but because the delegate implementation is coming from a third party framework i don't exactly where to put the setup code of dispatch for example:
group.enter()
and then:
func didFailToConnect(_ interface) {
group.leave()
}
Any ideas?

executeJavascript does not call completionHandler when inside a DispatchQueue

I've written a function that's supposed to return the HTML string that makes up a WKWebview. However, the completion handler is never called, and the project freezes indefinitely. I've also already adopted the WKScriptMessageHandler protocol so that's not the problem.
public func getHTML() -> String {
var result = ""
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: {(html: Any?, error: Error?) in
if (error != nil) {
print(error!)
}
result = html as! String
group.leave()
})
}
group.wait()
print("done waiting")
return result
}
I've found several examples on how to get the html, like here, but I don't want to merely print, I want to be able to return its value. I'm not experienced with DispatchQueues, but I do know for that WKWebView's evaluateJavaScript completion handler always runs on the main thread

fetchRecordCompletionBlock and semaphores - help understanding execution order

Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()

CLGeocoder() returns nil unexpectedly

I have a list of locations (about 30 elements):
var locations: [CLLocation] = [
CLLocation(latitude: 45.471172, longitude: 9.163317),
...
]
My purpose is to get street names from that list, so I decided to use CLGeocoder().
I call a function inside a viewDidLoad(), and every location is processed by lookUpCurrentLocation().
override func viewDidLoad() {
super.viewDidLoad()
for location in locations {
lookUpCurrentLocation(location: location, completionHandler: { streetName in
print(streetName)
})
}
}
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in
let placemark = placemarks?[0]
completionHandler(placemarks?[0].name)
})
}
My problem:
when the app starts, it prints a list of nil or only first two nil and the others street names.
terminal image 1
terminal image 2
I aspect to see the whole list processed without any nil.
Any hints?
As Leo said, you don’t want to run the requests concurrently. As the documentation says:
After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.
There are a few approaches to make these asynchronous requests run sequentially:
The simple solution is to make the method recursive, invoking the next call in the completion handler of the prior one:
func retrievePlacemarks(at index: Int = 0) {
guard index < locations.count else { return }
lookUpCurrentLocation(location: locations[index]) { name in
print(name ?? "no name found")
DispatchQueue.main.async {
self.retrievePlacemarks(at: index + 1)
}
}
}
And then, just call
retrievePlacemarks()
FWIW, I might use first rather than [0] when doing the geocoding:
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
completionHandler(placemarks?.first?.name)
}
}
I don’t think it’s possible for reverseGeocodeLocation to return a non-nil, zero-length array (in which case your rendition would crash with an invalid subscript error), but the above does the exact same thing as yours, but also eliminates that potential error.
An elegant way to make asynchronous tasks run sequentially is to wrap them in an asynchronous Operation subclass (such as a general-purpose AsynchronousOperation seen in the latter part of this answer).
Then you can define a reverse geocode operation:
class ReverseGeocodeOperation: AsynchronousOperation {
private static let geocoder = CLGeocoder()
let location: CLLocation
private var geocodeCompletionBlock: ((String?) -> Void)?
init(location: CLLocation, geocodeCompletionBlock: #escaping (String?) -> Void) {
self.location = location
self.geocodeCompletionBlock = geocodeCompletionBlock
}
override func main() {
ReverseGeocodeOperation.geocoder.reverseGeocodeLocation(location) { placemarks, _ in
self.geocodeCompletionBlock?(placemarks?.first?.name)
self.geocodeCompletionBlock = nil
self.finish()
}
}
}
Then you can create a serial operation queue and add your reverse geocode operations to that queue:
private let geocoderQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = Bundle.main.bundleIdentifier! + ".geocoder"
queue.maxConcurrentOperationCount = 1
return queue
}()
func retrievePlacemarks() {
for location in locations {
geocoderQueue.addOperation(ReverseGeocodeOperation(location: location) { string in
print(string ?? "no name found")
})
}
}
If targeting iOS 13 and later, you can use Combine, e.g. define a publisher for reverse geocoding:
extension CLGeocoder {
func reverseGeocodeLocationPublisher(_ location: CLLocation, preferredLocale locale: Locale? = nil) -> AnyPublisher<CLPlacemark, Error> {
Future<CLPlacemark, Error> { promise in
self.reverseGeocodeLocation(location, preferredLocale: locale) { placemarks, error in
guard let placemark = placemarks?.first else {
return promise(.failure(error ?? CLError(.geocodeFoundNoResult)))
}
return promise(.success(placemark))
}
}.eraseToAnyPublisher()
}
}
And then you can use a publisher sequence, where you specify maxPublishers of .max(1) to make sure it doesn’t perform them concurrently:
private var placemarkStream: AnyCancellable?
func retrievePlacemarks() {
placemarkStream = Publishers.Sequence(sequence: locations).flatMap(maxPublishers: .max(1)) { location in
self.geocoder.reverseGeocodeLocationPublisher(location)
}.sink { completion in
print("done")
} receiveValue: { placemark in
print("placemark:", placemark)
}
}
There are admittedly other approaches to make asynchronous tasks run sequentially (often involving calling wait using semaphores or dispatch groups), but I don’t think that those patterns are advisable, so I’ve excluded them from my list of alternatives, above.
Here's an implementation using Combine, with a persistent cache. Need more intelligent cache expiry logic, etc, but it is a starting point. Patches welcome.
https://gist.github.com/lhoward/dd6b64fb8f5782c933359e0d54bcb7d3

Correct way to perform async operations sequentially

I need to perform an async operation for each element in an array, one at at time. This operation calls back on the main queue.
func fetchResults(for: array, completion: () -> Void) {
var results: [OtherObject]: []
let queue = DispatchQueue(label: "Serial Queue")
queue.sync {
let group = DispatchGroup()
for object in array {
group.enter()
WebService().fetch(for: object) { result in
// Calls back on main queue
// Handle result
results.append(something)
group.leave()
}
group.wait()
}
}
print(results) // Never reached
completion()
}
The WebService call isn't calling back - which I think is telling me the main queue is blocked, but I can't understand why.
You should use group.notify() rather than group.wait(), since the latter is a synchronous, blocking operation.
I also don't see a point of dispatching to a queue if you only dispatch a single work item once.
func fetchResults(for: array, completion: () -> Void) {
var results: [OtherObject]: []
let group = DispatchGroup()
for object in array {
group.enter()
WebService().fetch(for: object) { result in
// Calls back on main queue
// Handle result
results.append(something)
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
print(results)
completion()
}
}
Maybe it's just a typo but basically don't run the queue synchronously.
Then instead of wait use notify outside(!) of the loop and print the results within the queue.
queue.async {
let group = DispatchGroup()
for object in array {
group.enter()
WebService().fetch(for: object) { result in
// Calls back on main queue
// Handle result
results.append(something)
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
print(results)
completion()
}
}
I d'ont think your main queue is locked, otherwise you would probably have an infinite loading on your app, as if it crashed ( in MacOS that's for sure ).
Here is what worked for me, maybe it will help :
class func synchronize(completion: #escaping (_ error: Bool) -> Void) {
DispatchQueue.global(qos: .background).async {
// Background Thread
var error = false
let group = DispatchGroup()
synchronizeObject1(group: group){ error = true }
synchronizeObject2(group: group){ error = true }
synchronizeObject3(group: group){ error = true }
group.wait() // will wait for everyone to sync
DispatchQueue.main.async {
// Run UI Updates or call completion block
completion(error)
}
}
}
class func synchronizeObject1(group: DispatchGroup, errorHandler: #escaping () -> Void){
group.enter()
WebservicesController.shared.getAllObjects1() { _ in
// Do My stuff
// Note: if an error occures I call errorHandler()
group.leave()
}
}
If I would say, it may come from the queue.sync instead of queue.async. But I'm not an expert on Asynchronous calls.
Hope it helps