swiftswiftuiwidgetkitweatherkit

How do I use WeatherKit Swift framework to fetch the weather inside a Widget Extension?


I can't fetch the weather using the WeatherKit framework inside a Widget Extension.

Here's a project I created just for this example. This widget shows the humidity of a sample location.

Here's the code:

import WidgetKit
import SwiftUI
import WeatherKit
import CoreLocation

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), humidity: 9.99) // placeholder humidity
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), humidity: 9.99) // placeholder humidity
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        Task {
            let nextUpdate = Date().addingTimeInterval(3600) // 1 hour in seconds
            
            let sampleLocation = CLLocation(latitude: 51.5072, longitude: 0.1276) // sample location (London)
            let weather = try await WeatherService.shared.weather(for: sampleLocation)
            let entry = SimpleEntry(date: .now, humidity: weather.currentWeather.humidity)
            
            let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
            completion(timeline)
        }
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let humidity: Double
}

struct WidgetWeatherTestWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text("\(entry.humidity)")
    }
}

struct WidgetWeatherTestWidget: 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([.systemMedium])
    }
}

It works fine in the simulator:

enter image description here

But it doesn't work in a real device. As soon as I call WeatherService's weather(for: CLLocation) async throws -> Weather the widget get's "blocked".

blocked widget

I've tried moving the try await WeatherService.shared.weather(for: sampleLocation) to different places, but nothing worked. As soon as I comment out this line (and create an entry with a placeholder humidity) everything works.

I added the WeatherKit capability in the main app target and in the WidgetExtension target. I also added the WeatherKit "Capability" and "App Service" in the "Certificates, Identifiers & Profiles" of the Apple Developer website.

The WeatherKit framework works fine in other parts of the main app.

I haven't tried using the WeatherKit REST API, yet.

Any ideas what could be happening?


Solution

  • You should set your entry to use Result, that way you can visualize any errors that are happening.

    struct SimpleEntry: TimelineEntry {
        let date: Date
        let configuration: ConfigurationIntent
        let result: Result<any CurrentWeatherProtocol, Error>
    }
    

    Then in the View you can either present the error or the weather view.

    struct PWCWidgetEntryView : View {
        var entry: Provider.Entry
    
        var body: some View {
            Text(entry.date, style: .time)
            switch entry.result{
            case .success(let weather):
                Image(systemName: weather.symbolName)
            case .failure(let error):
             
                Text(error.localizedDescription)
    
                let nserror = error as NSError
                Text(nserror.userInfo.description)
    
                Text("\(error)")
            }
        }
    }
    

    My whole code base is a part of a much more involved project but this is what the getTimeline looks like.

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        Task{
            let now = Date.now
            let inHr = Calendar.current.date(byAdding: .hour, value: 1, to: now)!
            do{
                
                let location = try await locSvc.requestSingleLocation()
                let wSvc = try await CDWeatherService().currentWeather(for: WeatherEntry(location: .init(location: location)))
                //Return a successful result
                let entry = SimpleEntry(date: now, configuration: configuration, result: .success(wSvc))
                let timeline = Timeline(entries: [entry], policy: .after(inHr))
                completion(timeline)
            }catch{
                //Return a failure result
                let entry = SimpleEntry(date: now, configuration: configuration, result: .failure(error))
                let timeline = Timeline(entries: [entry], policy: .after(inHr))
                completion(timeline)
            }
        }
    }
    

    2 things that I found to be my issues are macOS and iOS Widget Extensions, you have to make sure you pick the right one.

    Also, make sure you have added "WeatherKit" to the "Signing & Capabilities" of the Widget Extension.

    WeatherDaemon.WDSJWTAuthenticatorServiceListener.Errors error 2.

    Was related to not having Weather Kit included in "App Services" in AppStore Connect, it must be included in App Store Connect - Widget Identifier - Capabilities

    App Store Connect - Widget Identifier - AppServices

    Signing & Capabilities in Xcode for the App and/or Widget

    enter image description here