-1

The purpose of this code is that when you double tap on a MapKit view it will drop a pin on the map and get the latitude and longitude coordinates. When it drops the pin I want it to call an API and have that API return the city name. I want to display this city name by having a label over the map view that will update whenever you place a new pin.

When a pin is dropped this function is called:

func previewDataOnButton(){
    print("PreviewDataOnButton Called")
    theWeatherDataModel.callAPI(latitude: latitude!, longitude: longitude!)
    cityStateLabel.text = theWeatherDataModel.cityName
    print("PreviewDataOnButton Finished")
}

This function is in the view controller, and it calls a function in a separate model. The function it's calling looks like this:

func callAPI(latitude: String, longitude: String){
    let baseURL = "https://api.weatherbit.io/v2.0/current?&lat=\(latitude)&lon=\(longitude)&key=\(apiKey)"
    let urlComponents = URLComponents(string: baseURL)
    let theURL = urlComponents?.url
            
    let session = URLSession(configuration: .ephemeral)
    let theTask = session.dataTask(with: theURL!) {
        (data, response, error) in
        if let actualError = error{
            print("We got an error")
        } else if let actualData = data, let actualResponse = response{
            print("We got stuff back")
            let parsedData = try? JSON(data: actualData)
            //print("Data: \n\(String(describing: parsedData))")
            
            if let theWeatherArray = parsedData?["data"]{
                for(_, singleWeatherDictionary) in theWeatherArray{
                    
                    self.cityName = singleWeatherDictionary["city_name"].string
                }
            }
            
        } else {
            print("How did I get here?")
        }
        print("Im done with the closure")           
    }
    print("About to start the data task")
    theTask.resume()
    print("Started data task")
    
}

I placed print statements throughout the code to debug and it printed this:

PreviewDataOnButton Called

About to start the data task

Started data task

PreviewDataOnButton Finished

We got stuff back

Im done with the closure

From the output, it seems that the function in the view controller is finishing before the function in the model can finish its task and call the model. This is causing the label on the view to not update properly with the right city name because the function finishes before the API actually gets the city name. This is where I'm stuck, any help would be appreciated.

Community
  • 1
  • 1
jamesdamico
  • 9
  • 1
  • 2
  • 1
    Related: https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function – vadian May 12 '20 at 04:12
  • Related - https://stackoverflow.com/questions/46210879/returning-values-urlsession-shared-datatask-in-swift – sats May 12 '20 at 04:24
  • ... and the line `for(_, singleWeatherDictionary) in theWeatherArray{` is very confusing. It shows the dictionary enumeration syntax but `theWeatherArray` seems to be an array. The loop is pointless anyway because `self.cityName` is overwritten in each iteration. – vadian May 12 '20 at 04:24

2 Answers2

1

You should wait for the api call to get completed and then update the UI. Sleep you have won’t help. You can pass the UI update part as a completion block ( closure ) to the api call function and invoke that once the call is completed. You could end up with a code somewhat like below.

func previewDataOnButton(){
    print("PreviewDataOnButton Called")
    theWeatherDataModel.callAPI(latitude: latitude!, longitude: longitude!) { [weak self] completed in
        guard let self = self else {
            return
        }
        DispatchQueue.main.async {
            self.cityStateLabel.text = self.theWeatherDataModel.cityName
            print("PreviewDataOnButton Finished")
        }
    }
}
func callAPI(latitude: String, longitude: String, completionBlock: @escaping (Bool) -> Void){
    let baseURL = "https://api.weatherbit.io/v2.0/current?&lat=\(latitude)&lon=\(longitude)&key=\(apiKey)"
    let urlComponents = URLComponents(string: baseURL)
    let theURL = urlComponents?.url

    let session = URLSession(configuration: .ephemeral)
    let theTask = session.dataTask(with: theURL!) {
        (data, response, error) in
        if let actualError = error{
            print("We got an error")
            completionBlock(false)
        } else if let actualData = data, let actualResponse = response{
            print("We got stuff back")
            let parsedData = try? JSON(data: actualData)
            //print("Data: \n\(String(describing: parsedData))")

            if let theWeatherArray = parsedData?["data"]{
                for(_, singleWeatherDictionary) in theWeatherArray{

                    self.cityName = singleWeatherDictionary["city_name"].string
                }
            }
            completionBlock(true)

        } else {
            print("How did I get here?")
        }
        print("Im done with the closure")           
    }
    print("About to start the data task")
    theTask.resume()
    print("Started data task")

}
Subramanian Mariappan
  • 3,736
  • 1
  • 14
  • 29
0

You need add completion handler to your callAPI to wait until the function end. And within the function you need to use DispatchGroup to hold the function until for loop end. (It's no issue your theWeatherArray has few items. but it will issue when having large number of items)

func callAPI(latitude: String, longitude: String, completionHandler: @escaping(Result<Bool,Error>)->Void){
    let baseURL = "https://api.weatherbit.io/v2.0/current?&lat=\(latitude)&lon=\(longitude)&key=\(apiKey)"
    let urlComponents = URLComponents(string: baseURL)
    let theURL = urlComponents?.url

    let session = URLSession(configuration: .ephemeral)
    let theTask = session.dataTask(with: theURL!) {
        (data, response, error) in
        if let actualError = error{
            completionHandler(.failure(actualError))
        } else if let actualData = data, let actualResponse = response{

            let dGroup = DispatchGroup()
            let parsedData = try? JSON(data: actualData)

            if let theWeatherArray = parsedData?["data"]{
                for(_, singleWeatherDictionary) in theWeatherArray{
                    dGroup.enter()
                    self.cityName = singleWeatherDictionary["city_name"].string
                    dGroup.leave()
                }
            }

            dGroup.notify(queue: .main) {
                completionHandler(.success(true))
            }

        } else {
            completionHandler(.success(false))
        }
    }
    theTask.resume()
}

Now you call call this function like this

callAPI(latitude: "", longitude: "") { (response) in

        switch response{
        case .success(let granted):
            if granted{
                //success true
               cityStateLabel.text = theWeatherDataModel.cityName
            }else{
                //success false
            }
        case .failure(_):
            //error
        }
    }
Dilan
  • 2,610
  • 7
  • 23
  • 33