You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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应用有所不同,我推测可能是代码的触发顺序或位置存在问题,恳请各位提供帮助。


问题分析与修复方案

核心问题总结

  1. LocationManager生命周期过短:getTimeline内创建的LocationManager是局部变量,函数执行后立即销毁,无法接收定位回调。
  2. 异步任务时序错误:未等待定位和WeatherKit请求完成就生成Timeline,导致entry数据为空。
  3. @State使用错误:在非View环境下使用@State,无法正确管理状态。
  4. 全局变量未关联:全局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])
    }
}

关键修复点说明

  1. LocationManager改为单例:确保在Widget生命周期内持续存在,能接收定位回调。
  2. 使用withCheckedContinuation等待异步结果:在getTimeline中等待定位和城市名称获取完成,再请求天气数据,确保生成entry时数据已就绪。
  3. 移除无效的@State:改用普通变量和Combine监听@Published属性的变化。
  4. 错误处理增强:天气请求失败时生成带错误提示的entry,避免Widget空白。
  5. UI优化:调整Widget布局,添加背景和间距,提升显示效果。

内容的提问来源于stack exchange,提问作者BenG94

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:35:21