Trouble with @escaping Competion Handler when passing data in a shared data container

Hi,

I am coding my first App and have never coded before and need some guidance with the @escaping completion handler logic.

I am trying to get the count of fires from a third party API and then pass the count using a shared Data Container class to my main View Controller.

  • The issue is that when I call the func loadInitialFireMapData on my main View Controller, I am getting an error "Expected expression in list of expressions"

  • This is because I don't quite understand yet how to call a function when there is a completion handler

  • The @escaping completion handler is necessary since the main View Controller needs to wait for the API call (func loadInitialFireMapData) to finish in order to get the fire count.

  • Any advice would be greatly appreciated!

See the code from the Main View Controller and the API call with the @ escaping function from my FireDataManager.swift file.

Here is the code from my Main View Controller:

// ViewController.swift
class DataContainer {
    static let shared = DataContainer()

    var fireCount: Int = 0
    var fires: [Fire] = []
    var totalFireCount: Int = 0
}



class ViewController: UIViewController, CLLocationManagerDelegate {

    let dataContainer = DataContainer.shared
    let fireDataManager = FireDataManager ()

    override func viewDidLoad() {
        super.viewDidLoad()

//Retrieve Fire API data from Fire Data Manager
        fireDataManager.loadInitialFireMapData (completion: ([Fire]) -> return)  {

            self.FireStatusLabel.text = (String(DataContainer.shared.totalFireCount) + " fires within 100 miles of your location.")
        }

Here is the FireDataManager.swift file with the API call/@escaping completion handler function.

//  FireDataManager.swift  

class FireDataManager {

func loadInitialFireMapData(completion: @escaping () -> Swift.Void) {
        if let url = URL(string:

        "https://opendata.arcgis.com/datasets/68637d248eb24d0d853342cba02d4af7_0.geojson")

        {

            URLSession.shared.dataTask(with: url) {data, response, error in
                if let data = data {
                       do {
                        let features = try MKGeoJSONDecoder().decode(data)
                               .compactMap { $0 as? MKGeoJSONFeature }
                            let validWorks = features.compactMap(Fire.init)
                        DataContainer.shared.fires.append(contentsOf: validWorks)
                        DataContainer.shared.totalFireCount = validWorks.count
                        print("Fire Data Manager Count of Total Fires: ", DataContainer.shared.totalFireCount)
                        DispatchQueue.main.async {
                            completion()
                        }

                       }
                       catch let error {
                        print("FireMap URL Session error: ", error.localizedDescription)
                       }
                    }
                 }
           .resume()
        }

    }
}
Answered by OOPer in 679131022

Any advice would be greatly appreciated!

If you can target your app for iOS 15+ and are planning to release it after the released version of Xcode 13 is out, you may try using new async/await feature.

(There are more session videos about async/await and I recommend to watch all if you have enough time.)


If you need to make your app target iOS 14.x and earlier, working with completion handler would be necessary.

I assume your loadInitialFireMapData is working fine in normal cases.

(Hope you are not ignoring any warnings.)

But it has some flaws considering error cases:

  • It ignores some error cases without showing any debug info
  • It does not call completion handler on errors

You should better pass an Optional<Error> to the completion handler to indicate error cases.

I would write it as follows:

class FireDataManager {
    
    enum Errors: Error {
        case urlInvalid
        case dataIsNil
    }
    
    func loadInitialFireMapData(completion: @escaping (Error?) -> Void) { //<-
        guard let url = URL(string:
                                "https://opendata.arcgis.com/datasets/68637d248eb24d0d853342cba02d4af7_0.geojson") else {
            completion(Errors.urlInvalid)
            return
        }
        URLSession.shared.dataTask(with: url) {data, response, error in
            if let error = error {
                print("FireMap URL Session error: ", error) //Use `error` instead of `error.localizedDescription` to show debug info
                completion(error)
                return
            }
            guard let data = data else {
                completion(Errors.dataIsNil)
                return
            }
            do {
                let features = try MKGeoJSONDecoder().decode(data)
                    .compactMap { $0 as? MKGeoJSONFeature }
                let validWorks = features.compactMap(Fire.init)
                DataContainer.shared.fires.append(contentsOf: validWorks)
                DataContainer.shared.totalFireCount = validWorks.count
                print("Fire Data Manager Count of Total Fires: ", DataContainer.shared.totalFireCount)
                DispatchQueue.main.async {
                    completion(nil) //<- no error here
                }
            } catch let error {
                print("FireMap decoding error: ", error)
                completion(error)
            }
        }
        .resume()
    }
}

In this case, meaning the type of completion is (Error?)->Void, you need to write a closure like { (error: Error?)->Void in ... }

or in a simplified form { error in ... } .

So, the caller side code would be something like this:

class ViewController: UIViewController, CLLocationManagerDelegate {
    @IBOutlet weak var fireStatusLabel: UILabel!

    let dataContainer = DataContainer.shared
    let fireDataManager = FireDataManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        //Retrieve Fire API data from Fire Data Manager
        fireDataManager.loadInitialFireMapData (completion: { error in
            if let error = error {
                print(error)
                return
            }
            self.fireStatusLabel.text = "\(DataContainer.shared.totalFireCount) fires within 100 miles of your location."
        })
        //...
    }
    
    //...
}

(I renamed FireStatusLabel to fireStatusLabel as only type names start with Capital letter in Swift. If you have any reasons you cannot rename it, please re-interpret the lines with fireStatusLabel.)

Or you can use the trailing closure notation like this:

        fireDataManager.loadInitialFireMapData { error in //<- no opening parenthesis here
            if let error = error {
                print(error)
                return
            }
            self.fireStatusLabel.text = "\(DataContainer.shared.totalFireCount) fires within 100 miles of your location."
        } //<- no closing parenthesis

Please try.

Accepted Answer

Any advice would be greatly appreciated!

If you can target your app for iOS 15+ and are planning to release it after the released version of Xcode 13 is out, you may try using new async/await feature.

(There are more session videos about async/await and I recommend to watch all if you have enough time.)


If you need to make your app target iOS 14.x and earlier, working with completion handler would be necessary.

I assume your loadInitialFireMapData is working fine in normal cases.

(Hope you are not ignoring any warnings.)

But it has some flaws considering error cases:

  • It ignores some error cases without showing any debug info
  • It does not call completion handler on errors

You should better pass an Optional<Error> to the completion handler to indicate error cases.

I would write it as follows:

class FireDataManager {
    
    enum Errors: Error {
        case urlInvalid
        case dataIsNil
    }
    
    func loadInitialFireMapData(completion: @escaping (Error?) -> Void) { //<-
        guard let url = URL(string:
                                "https://opendata.arcgis.com/datasets/68637d248eb24d0d853342cba02d4af7_0.geojson") else {
            completion(Errors.urlInvalid)
            return
        }
        URLSession.shared.dataTask(with: url) {data, response, error in
            if let error = error {
                print("FireMap URL Session error: ", error) //Use `error` instead of `error.localizedDescription` to show debug info
                completion(error)
                return
            }
            guard let data = data else {
                completion(Errors.dataIsNil)
                return
            }
            do {
                let features = try MKGeoJSONDecoder().decode(data)
                    .compactMap { $0 as? MKGeoJSONFeature }
                let validWorks = features.compactMap(Fire.init)
                DataContainer.shared.fires.append(contentsOf: validWorks)
                DataContainer.shared.totalFireCount = validWorks.count
                print("Fire Data Manager Count of Total Fires: ", DataContainer.shared.totalFireCount)
                DispatchQueue.main.async {
                    completion(nil) //<- no error here
                }
            } catch let error {
                print("FireMap decoding error: ", error)
                completion(error)
            }
        }
        .resume()
    }
}

In this case, meaning the type of completion is (Error?)->Void, you need to write a closure like { (error: Error?)->Void in ... }

or in a simplified form { error in ... } .

So, the caller side code would be something like this:

class ViewController: UIViewController, CLLocationManagerDelegate {
    @IBOutlet weak var fireStatusLabel: UILabel!

    let dataContainer = DataContainer.shared
    let fireDataManager = FireDataManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        //Retrieve Fire API data from Fire Data Manager
        fireDataManager.loadInitialFireMapData (completion: { error in
            if let error = error {
                print(error)
                return
            }
            self.fireStatusLabel.text = "\(DataContainer.shared.totalFireCount) fires within 100 miles of your location."
        })
        //...
    }
    
    //...
}

(I renamed FireStatusLabel to fireStatusLabel as only type names start with Capital letter in Swift. If you have any reasons you cannot rename it, please re-interpret the lines with fireStatusLabel.)

Or you can use the trailing closure notation like this:

        fireDataManager.loadInitialFireMapData { error in //<- no opening parenthesis here
            if let error = error {
                print(error)
                return
            }
            self.fireStatusLabel.text = "\(DataContainer.shared.totalFireCount) fires within 100 miles of your location."
        } //<- no closing parenthesis

Please try.

Trouble with @escaping Competion Handler when passing data in a shared data container
 
 
Q