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()
}
}
}
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.