SwiftUI中为Widget集成WeatherKit遇空白问题求助
问题:Widget集成WeatherKit后显示空白无数据
我有一个天气应用的Widget,当前应用尚未使用WeatherKit,但希望先为该Widget集成WeatherKit框架。我已制作了一个使用WeatherKit的测试应用,Widget所需的所有组件均能正常工作。
可正常运行的主应用代码
import SwiftUI import CoreLocation import WeatherKit var userCity: String = "" class LocationManager: NSObject, ObservableObject { @Published var currentLocation: CLLocation? @Published var userCity: String = "" private let locationManager = CLLocationManager() override init() { super.init() locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.distanceFilter = kCLDistanceFilterNone locationManager.requestAlwaysAuthorization() locationManager.startUpdatingLocation() locationManager.delegate = self } } extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, currentLocation == nil else { return } DispatchQueue.main.async { self.currentLocation = location } CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in print(location) guard error == nil else { print("Reverse geocoder failed with error" + error!.localizedDescription) return } guard placemarks!.count > 0 else { print("Problem with the data received from geocoder") return } let pm = placemarks![0].locality print(pm!) let userLocation = (pm!) print(userLocation) DispatchQueue.main.async { self.userCity = userLocation } }) } } struct ContentView: View { let weatherService = WeatherService.shared @StateObject private var locationManager = LocationManager() @State private var weather: Weather? var body: some View { VStack { if let weather{ let celsiusWeather = weather.currentWeather.temperature.converted(to: .celsius) let celsiusFormatted = String(format: "%.0f", celsiusWeather.value) let celsiusFinal = "\(celsiusFormatted)°C" let fahrenheitWeather = weather.currentWeather.temperature.converted(to: .fahrenheit) let fahrenheitFormatted = String(format: "%.0f", fahrenheitWeather.value) let fahrenheitFinal = "\(fahrenheitFormatted)°F" VStack{ Image(systemName: weather.currentWeather.symbolName) .resizable() .symbolRenderingMode(.palette) .scaledToFit() .frame(width: 80, height: 80) Text(locationManager.userCity) .font(.largeTitle) Text(fahrenheitFinal) Text(celsiusFinal) } } } .padding() .task(id: locationManager.currentLocation) { do{ if let location = locationManager.currentLocation{ self.weather = try await weatherService.weather(for: location) print(weather!) } }catch{ print(error) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
我尝试将上述代码迁移到Widget中,但Widget显示空白,无法获取天气数据,且Xcode未给出任何报错信息。
尝试编写的Widget代码
import WidgetKit import SwiftUI import WeatherKit import CoreLocation var userCity: String? class LocationManager: NSObject, ObservableObject { @Published var currentLocation: CLLocation? @Published var userCity: String = "" private let locationManager = CLLocationManager() override init() { super.init() locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.distanceFilter = kCLDistanceFilterNone locationManager.requestAlwaysAuthorization() locationManager.startUpdatingLocation() locationManager.delegate = self } } extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, currentLocation == nil else { return } DispatchQueue.main.async { self.currentLocation = location } CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in print(location) guard error == nil else { print("Reverse geocoder failed with error" + error!.localizedDescription) return } guard placemarks!.count > 0 else { print("Problem with the data received from geocoder") return } let pm = placemarks![0].locality print(pm!) let userLocation = (pm!) print(userLocation) DispatchQueue.main.async { self.userCity = userLocation } }) } } struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let locationManager = LocationManager() @State var currentWeather: Weather? let nextUpdate = Date().addingTimeInterval(1800) Task { do{ if let location = locationManager.currentLocation{ let currentWeather = try await WeatherService.shared.weather(for: location) print(currentWeather) } }catch{ print(error) } } let entry = SimpleEntry(date: .now, city: userCity!, temp: (currentWeather?.currentWeather.temperature.description)!) let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let city: String let temp: String } struct WidgetWeatherTestWidgetEntryView : View { var entry: Provider.Entry var body: some View { VStack{ Text(entry.city) Text(entry.temp) } } } struct ShitsdagWidget: Widget { let kind: String = "WidgetWeatherTest" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in WidgetWeatherTestWidgetEntryView(entry: entry) } .configurationDisplayName("Widget Weather Test") .description("Testing weatherkit in a Widget") .supportedFamilies([.systemSmall]) } }
由于Widget的Timeline等机制与普通SwiftUI应用有所不同,我推测可能是代码的触发顺序或位置存在问题,恳请各位提供帮助。
问题分析与修复方案
核心问题总结
- LocationManager生命周期过短:
getTimeline内创建的LocationManager是局部变量,函数执行后立即销毁,无法接收定位回调。 - 异步任务时序错误:未等待定位和WeatherKit请求完成就生成Timeline,导致entry数据为空。
- @State使用错误:在非View环境下使用@State,无法正确管理状态。
- 全局变量未关联:全局
userCity未被LocationManager的回调更新,数据传递断裂。
修复后的Widget代码
import WidgetKit import SwiftUI import WeatherKit import CoreLocation // 将LocationManager改为单例,确保生命周期稳定 class LocationManager: NSObject, ObservableObject { static let shared = LocationManager() @Published var currentLocation: CLLocation? @Published var userCity: String = "" private let locationManager = CLLocationManager() private override init() { super.init() locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.distanceFilter = kCLDistanceFilterNone locationManager.requestAlwaysAuthorization() locationManager.startUpdatingLocation() locationManager.delegate = self } } extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, currentLocation == nil else { return } DispatchQueue.main.async { self.currentLocation = location } CLGeocoder().reverseGeocodeLocation(location) { [weak self] placemarks, error in guard let self = self else { return } print(location) guard error == nil else { print("Reverse geocoder failed with error: \(error!.localizedDescription)") return } guard let pm = placemarks?.first?.locality else { print("Problem with the data received from geocoder") return } print(pm) DispatchQueue.main.async { self.userCity = pm } } } } struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), city: "加载中...", temp: "--°") } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), city: "加载中...", temp: "--°") completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let locationManager = LocationManager.shared let nextUpdate = Date().addingTimeInterval(1800) // 使用Task等待定位和天气请求完成 Task { // 等待定位结果 let location: CLLocation = await withCheckedContinuation { continuation in if let existingLocation = locationManager.currentLocation { continuation.resume(returning: existingLocation) } else { // 监听定位更新 let cancellable = locationManager.$currentLocation.sink { newLocation in if let newLocation = newLocation { continuation.resume(returning: newLocation) cancellable.cancel() } } } } // 等待城市名称 let city: String = await withCheckedContinuation { continuation in if !locationManager.userCity.isEmpty { continuation.resume(returning: locationManager.userCity) } else { let cancellable = locationManager.$userCity.sink { city in if !city.isEmpty { continuation.resume(returning: city) cancellable.cancel() } } } } // 请求天气数据 do { let weather = try await WeatherService.shared.weather(for: location) let temp = String(format: "%.0f°", weather.currentWeather.temperature.converted(to: .celsius).value) let entry = SimpleEntry(date: .now, city: city, temp: temp) let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } catch { print("Weather request failed: \(error)") // 出错时显示默认entry let entry = SimpleEntry(date: .now, city: "获取失败", temp: "--°") let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } } } } struct SimpleEntry: TimelineEntry { let date: Date let city: String let temp: String } struct WidgetWeatherTestWidgetEntryView : View { var entry: Provider.Entry var body: some View { VStack(spacing: 4) { Text(entry.city) .font(.caption) .foregroundColor(.secondary) Text(entry.temp) .font(.title) .fontWeight(.semibold) } .padding() } } struct ShitsdagWidget: Widget { let kind: String = "WidgetWeatherTest" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in WidgetWeatherTestWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Widget Weather Test") .description("Testing weatherkit in a Widget") .supportedFamilies([.systemSmall]) } }
关键修复点说明
- LocationManager改为单例:确保在Widget生命周期内持续存在,能接收定位回调。
- 使用
withCheckedContinuation等待异步结果:在getTimeline中等待定位和城市名称获取完成,再请求天气数据,确保生成entry时数据已就绪。 - 移除无效的@State:改用普通变量和Combine监听@Published属性的变化。
- 错误处理增强:天气请求失败时生成带错误提示的entry,避免Widget空白。
- UI优化:调整Widget布局,添加背景和间距,提升显示效果。
内容的提问来源于stack exchange,提问作者BenG94
相关产品推荐
相关产品推荐

