7.3 — WeatherKit

Opening scenario

The product team says: “Add the weather to the trip planner. Just like Dark Sky used to do.” You used to ping Dark Sky’s REST API for $0.0001 per call. Apple bought Dark Sky in 2020, deprecated the public API in March 2023, and rolled the data into WeatherKit — a Swift-native framework with 500,000 free calls per month per developer team, then $49.99/mo per million. Same forecast data, different ergonomics: type-safe, async/await, and bundled with proper Apple privacy controls.

ContextWhat it usually means
Reads “WeatherService”Knows the entry point
Reads “current/hourly/daily/alerts”Has worked with the data shapes
Reads “WeatherKit entitlement”Has wired the capability
Reads “attribution”Knows Apple requires the badge
Reads “REST endpoint”Has used the server-side WeatherKit too

Concept → Why → How → Code

Concept

WeatherKit is a Swift-only async API over Apple’s weather data (formerly Dark Sky’s). One call returns a Weather object holding sub-objects: currentWeather, minuteForecast, hourlyForecast, dailyForecast, weatherAlerts, and availability. Request only the slices you need to reduce battery and quota usage.

Why

  • Native Swift, no DTO boilerplate.
  • Free tier generous enough for most apps.
  • Privacy-respecting — no third-party data sharing.
  • Server-side REST also available with the same auth key for cross-platform backends.

How — entitlement & first call

  1. Apple Developer portal → Identifiers → your App ID → enable WeatherKit.
  2. Project → Signing & Capabilities → + Capability → WeatherKit.
  3. Info.plist → no key required (no permission prompt — uses CLLocation permission you already requested).
import WeatherKit
import CoreLocation

let service = WeatherService.shared
let nyc = CLLocation(latitude: 40.7128, longitude: -74.0060)
let weather = try await service.weather(for: nyc)
print(weather.currentWeather.temperature.formatted(.measurement(width: .abbreviated)))
print(weather.dailyForecast.first?.condition.description ?? "")

weather.currentWeather.temperature is a Measurement<UnitTemperature> — formatting respects locale & units.

Requesting a subset

Save quota and latency:

let (current, hourly) = try await service.weather(
    for: nyc,
    including: .current, .hourly
)

The variadic including: returns a tuple matching the order of the keypaths. Each slice is independently typed:

  • .currentCurrentWeather
  • .minuteForecast<MinuteWeather>? (next hour; nil outside US)
  • .hourlyForecast<HourWeather>
  • .dailyForecast<DayWeather>
  • .alerts[WeatherAlert]?
  • .availabilityWeatherAvailability

Wiring with CoreLocation

import Observation

@Observable
@MainActor
final class WeatherViewModel {
    var summary: String = "—"
    var error: String?

    func refresh(for location: CLLocation) async {
        do {
            let weather = try await WeatherService.shared.weather(for: location)
            let temp = weather.currentWeather.temperature
            let condition = weather.currentWeather.condition.description
            summary = "\(temp.formatted(.measurement(width: .abbreviated))) • \(condition)"
        } catch {
            self.error = error.localizedDescription
        }
    }
}

Use CLLocationManager (Chapter 7.4) to obtain the user’s CLLocation before calling.

Attribution (required)

Apple requires you to display attribution near the weather data:

let attribution = try await WeatherService.shared.attribution
AsyncImage(url: attribution.combinedMarkLightURL) { img in
    img.resizable().scaledToFit()
} placeholder: { Color.clear }
.frame(height: 12)

Link("Other data sources", destination: attribution.legalPageURL)
    .font(.caption2)

Skip this and Apple may revoke your entitlement. Apple checks during App Review.

Caching strategy

actor WeatherCache {
    private var cache: [String: (Weather, Date)] = [:]
    private let ttl: TimeInterval = 600   // 10 minutes

    func weather(at location: CLLocation) async throws -> Weather {
        let key = "\(Int(location.coordinate.latitude * 100)),\(Int(location.coordinate.longitude * 100))"
        if let (cached, date) = cache[key], Date.now.timeIntervalSince(date) < ttl {
            return cached
        }
        let fresh = try await WeatherService.shared.weather(for: location)
        cache[key] = (fresh, .now)
        return fresh
    }
}

Round the coordinates to ~1km buckets so a slowly-walking user doesn’t burn quota.

Server-side REST

For backends (and non-Apple clients) Apple exposes WeatherKit as a REST API. Auth uses the same .p8 key flow as APNs: sign a JWT with your WeatherKitTeam key, send it as a Bearer token, hit https://weatherkit.apple.com/api/v1/weather/{lang}/{lat}/{lon}.

In the wild

  • Apple Weather itself uses WeatherKit + the deprecated Dark Sky models.
  • Carrot Weather, CARROT, Weather Strip — all migrated from Dark Sky to WeatherKit during the 2022–2023 transition.
  • Strava uses WeatherKit on iOS to show conditions during a run.
  • Hopper (travel) integrates WeatherKit for destination forecasts.
  • Apple Watch uses the same framework via watchOS.

Common misconceptions

  1. “WeatherKit is a system app.” It’s a framework; you must add the capability and your app’s entitlement bundles a per-team auth key.
  2. “WeatherKit needs its own location permission.” It uses whatever CLLocation you pass in. You still need NSLocationWhenInUseUsageDescription from CoreLocation to get the location.
  3. “500K calls is unlimited.” It’s per developer team, not per app. A studio with 10 apps shares the budget. Above the cap you’re billed (or throttled if no payment method).
  4. weatherAlerts works everywhere.” Coverage is country-dependent (US/Canada/EU/UK/JP/AU/NZ). Always check weather.availability.alertAvailability.
  5. “WeatherKit can deliver via push when conditions change.” No. It’s pull-only. Build your own background-refresh-and-notify pipeline.

Seasoned engineer’s take

The most underrated thing about WeatherKit isn’t the data; it’s the types. After years of decoding JSON shapes like {"icon":"partly-cloudy-day","temperature":72.4} and writing your own enum WeatherIcon, getting weather.currentWeather.symbolName (an SF Symbol name) and weather.currentWeather.condition.description (localized) is a quiet joy. Lean into that — pass Measurement<UnitTemperature> and Date down to your views, don’t pre-convert. The formatters localize for you.

Two practical things:

  1. Bucket your locations before calling. Users who move 5m don’t need a new forecast. We’ve seen apps burn through 500K quota in a week by calling on every CLLocationManager update.
  2. Cache aggressively (10–30 min) and prefer subset requests. A widget that needs current only should not fetch hourly+daily+alerts.

TIP: Use weather.currentWeather.symbolName as the Image(systemName:) value — Apple maintains the mapping so conditions like “mostly cloudy at night” stay aligned with their visual symbol.

WARNING: WeatherKit’s availability object isn’t decorative. In several countries minute-by-minute precipitation is nil; in others, alerts return empty arrays despite active storms. Always check availability before promising features to your designer.

Interview corner

Junior: “How do you get the current temperature for a user’s location?”

Request CLLocationWhenInUseUsageDescription and use CLLocationManager to get a CLLocation. Add the WeatherKit capability, then call WeatherService.shared.weather(for: location).currentWeather.temperature. Display attribution per Apple’s requirement.

Mid: “How do you minimize API quota use across an app that powers a widget, a Live Activity, and the main app?”

Centralize in a single shared actor backed by a coordinate-bucketed cache (~1km, 10-min TTL). The widget and main app share via an App Group — widget reads pre-computed data the host app populated. The Live Activity pulls minute-cast or hourly only as needed and updates via APNs from your own backend that uses the REST WeatherKit endpoint (also caches). Request only the slices you need with the variadic including: API.

Senior: “Design a notification system that pings 5 million users when severe weather is forecast for their location.”

Don’t call WeatherKit per user every hour — that’s 120M calls/day, well above quota and absurdly wasteful. Build a server-side pipeline: cluster users by geohash (~5km), call the REST WeatherKit endpoint per cluster on a schedule, materialize alerts into a queue, fan out APNs pushes to users in affected clusters. Critical alerts use the critical interruption level (with Apple-granted entitlement). On-device, the app subscribes via APNs and verifies alert relevance using last-known coords before showing. Audit: log per-cluster call counts, ensure quota isn’t exceeded. Cache: serve repeated reads of the same cluster from a Redis-style cache with a 10-min TTL.

Red flag: “We poll WeatherKit every 5 minutes from the app’s foreground.”

Burns battery, burns quota, and isn’t even fresh — the underlying data updates less often than that. Polling is almost never the right pattern for weather; either cache + on-demand, or push-driven from your backend.

Lab preview

Lab 7.1 — Weather + Map app builds a SwiftUI screen overlaying current conditions, hourly forecast, and active alerts on a MapKit canvas with location-following annotations.


Next: 7.4 — MapKit & CoreLocation