async api call not returning in swift widget

Viewed 42

Im trying to get my weather app to display the weather on a widget. I have no problems with fetching the data in my app itself however for some reason when calling it from the widget, its not getting into the timeline. Here is my func to fetch the weather.

func getCurrentWeather(latitude : CLLocationDegrees, longitude : CLLocationDegrees)  async throws -> ResponseBody
{
    let forReal = "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=&units=metric"
    guard let url = URL(string: forReal) else {fatalError("MISSING URL")}

    let urlrequest = URLRequest(url: url)
    let (data, response) = try await URLSession.shared.data(for: urlrequest)
    
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("ERROR FETCHING CURRENT WEATHER")}
    let decodedData = try JSONDecoder().decode(ResponseBody.self, from: data)
    print(decodedData)
    return decodedData
}

and here is my getTimeline

@State var weather: ResponseBody?
var weatherManager = WeatherManager()

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []
    widgetLocationManager.fetchLocation(handler: { location in
            print(location) })
    
    if let location = widgetLocationManager.locationManager?.location
    {
        Task
        {
            do {
            weather =  try await weatherManager.getCurrentWeather(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
            } catch {  print("ERROR GETTING WEATHER:", error)  }
        }
        if let weather = weather
        {
            entries.append(SimpleEntry(date: Date(), feelslike: weather.main.feelsLike, description: weather.weather[0].description))
            entries.append(SimpleEntry(date: Calendar.current.date(byAdding: .minute, value: 60, to: Date())!, feelslike: weather.main.feelsLike, description: weather.weather[0].description))
        } else
        {
            print("no weather")
        }
    }
    else {
        print("No location")
    }
    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}

I am getting a location and I know my api call is working however I think the problem is that it is coming through after I've already checked if its been fetched. At least thats the order that its getting printed in the console. I thought Task{} was async and so would execute that function and wait until completion before moving on? My console returns

No location
no weather

followed by 4 location fetches and then a weather api fetch however its not managing to get into my timeline for some reason. Probably missing something super obvious but appreciate the help all the same :)

1 Answers

You should probably look up how concurrent coding with completionHandlers and how Task work.

E.g.:

widgetLocationManager.fetchLocation(handler: { location in
        print(location) 
    })
    

// This line will run before `fetchLocation(...` has run, so you need to move it inside
// the completion closure of `fetchLocation`
if let location = widgetLocationManager.locationManager?.location
{

The same at this point:

Task{
    do {
    weather =  try await weatherManager.getCurrentWeather(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
    } catch {  print("ERROR GETTING WEATHER:", error)  }
}
// the code inside task may run synchronous in its own thread
// but this line of code will execute concurrently at the same time
// so weather will be nil the first time
if let weather = weather
{

Here I fixed these errors:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    var entries: [SimpleEntry] = []
    
    widgetLocationManager.fetchLocation{ location in
        print(location)
        
        guard let location = location else{
            print("No location")
            return
        }
        
        Task
        {
            do {
                weather =  try await weatherManager.getCurrentWeather(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
            } catch {  print("ERROR GETTING WEATHER:", error)  }
            
            guard let weather = weather else{
                print("no weather")
                return
            }
            
            entries.append(SimpleEntry(date: Date(), feelslike: weather.main.feelsLike, description: weather.weather[0].description))
            entries.append(SimpleEntry(date: Calendar.current.date(byAdding: .minute, value: 60, to: Date())!, feelslike: weather.main.feelsLike, description: weather.weather[0].description))
            
            let timeline = Timeline(entries: entries, policy: .atEnd)
            completion(timeline)
        }
    }
}

But

I cannot guarantee this will work as intended. You would need to provide a minimal reproducible example so we can debug and check. It will at least point you in the right direction how this should be structured.

WWDC asyn/await

Swift docs closures

Related