Swift Widget异步API调用未返回,无法加载至Timeline求助
天气Widget异步数据加载问题排查与解决
问题描述
开发天气App的Widget组件时,App内可正常获取天气数据,但Widget中调用异步API后,数据无法加载到Timeline中。控制台先输出No location和no weather,之后才会获取到位置数据和天气API请求结果,但数据始终无法进入Timeline。
相关代码
获取天气的异步方法
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 }
Widget的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) }
问题根源
- 位置获取的异步处理错误:
widgetLocationManager.fetchLocation是异步回调方法,代码中直接同步判断widgetLocationManager.locationManager?.location是否存在,此时位置还未获取到,所以先输出No location。 - Task异步执行不阻塞主线程:
Task{}中的异步代码是并发执行的,不会等待它完成就继续往下执行后续逻辑,此时weather还是nil,所以输出no weather,并直接调用completion返回空的Timeline。后续异步任务完成时,Timeline已经生成并返回,无法再更新。 - @State在Widget中无效:Widget的
TimelineProvider是结构体,@State属性不适合用在这里,且getTimeline是一次性执行的方法,状态不会被保留。 - fatalError会导致Widget崩溃:获取天气的方法中使用
fatalError,在Widget环境中会直接导致组件崩溃,无法优雅处理错误。
解决方案
1. 修正位置获取的异步逻辑
将后续天气请求逻辑放到位置获取的回调中,确保拿到有效位置后再处理天气数据。
2. 正确处理异步天气请求
在位置回调内启动Task,等待天气请求完成后再创建Timeline并调用completion,确保数据正确传入Entry。
3. 替换fatalError为自定义错误
避免Widget崩溃,改用抛出自定义错误并在上层捕获处理。
修改后的代码
自定义天气错误枚举
enum WeatherError: Error { case invalidURL case invalidResponse case decodingFailed(Error) }
修正后的getCurrentWeather方法
func getCurrentWeather(latitude : CLLocationDegrees, longitude : CLLocationDegrees) async throws -> ResponseBody { let urlString = "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=&units=metric" guard let url = URL(string: urlString) else { throw WeatherError.invalidURL } let urlrequest = URLRequest(url: url) let (data, response) = try await URLSession.shared.data(for: urlrequest) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw WeatherError.invalidResponse } do { let decodedData = try JSONDecoder().decode(ResponseBody.self, from: data) print(decodedData) return decodedData } catch { throw WeatherError.decodingFailed(error) } }
修正后的getTimeline方法
var weatherManager = WeatherManager() func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { // 依赖异步位置回调,确保拿到位置后再处理 widgetLocationManager.fetchLocation(handler: { location in guard let location = location else { print("No location received") // 返回默认状态的Entry let defaultEntry = SimpleEntry(date: Date(), feelslike: 0, description: "无法获取位置") let timeline = Timeline(entries: [defaultEntry], policy: .atEnd) completion(timeline) return } // 在位置回调内启动异步任务获取天气 Task { do { let weather = try await self.weatherManager.getCurrentWeather( latitude: location.coordinate.latitude, longitude: location.coordinate.longitude ) // 天气获取成功,创建Entry let currentDate = Date() guard let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 60, to: currentDate) else { throw WeatherError.invalidResponse } let entries = [ SimpleEntry(date: currentDate, feelslike: weather.main.feelsLike, description: weather.weather[0].description), SimpleEntry(date: nextUpdateDate, feelslike: weather.main.feelsLike, description: weather.weather[0].description) ] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } catch { print("ERROR GETTING WEATHER:", error) // 返回错误状态的Entry let errorEntry = SimpleEntry(date: Date(), feelslike: 0, description: "获取天气失败") let timeline = Timeline(entries: [errorEntry], policy: .atEnd) completion(timeline) } } }) }
关键修改说明
- 移除了无效的
@State变量,改用局部变量存储天气数据 - 将所有逻辑嵌套到位置获取的回调中,确保获取到位置后再执行天气请求
- 在
Task中等待天气请求完成后才创建Timeline并调用completion,保证数据正确加载 - 增加了错误处理的 fallback,无论位置或天气请求失败,都返回有效的Timeline
- 替换
fatalError为自定义错误,避免Widget崩溃
内容的提问来源于stack exchange,提问作者Matt Pidden
相关产品推荐
相关产品推荐

