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

Jim H. picture Jim H. · Aug 6, 2017 · Viewed 8.2k times · Source

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

}

Answer

Rob picture Rob · Aug 6, 2017

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.