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.
| Context | What 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
- Apple Developer portal → Identifiers → your App ID → enable WeatherKit.
- Project → Signing & Capabilities → + Capability → WeatherKit.
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:
.current→CurrentWeather.minute→Forecast<MinuteWeather>?(next hour; nil outside US).hourly→Forecast<HourWeather>.daily→Forecast<DayWeather>.alerts→[WeatherAlert]?.availability→WeatherAvailability
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
- “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.
- “WeatherKit needs its own location permission.” It uses whatever
CLLocationyou pass in. You still needNSLocationWhenInUseUsageDescriptionfrom CoreLocation to get the location. - “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).
- “
weatherAlertsworks everywhere.” Coverage is country-dependent (US/Canada/EU/UK/JP/AU/NZ). Always checkweather.availability.alertAvailability. - “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:
- 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
CLLocationManagerupdate. - Cache aggressively (10–30 min) and prefer subset requests. A widget that needs
currentonly should not fetch hourly+daily+alerts.
TIP: Use
weather.currentWeather.symbolNameas theImage(systemName:)value — Apple maintains the mapping so conditions like “mostly cloudy at night” stay aligned with their visual symbol.
WARNING: WeatherKit’s
availabilityobject isn’t decorative. In several countries minute-by-minute precipitation isnil; in others, alerts return empty arrays despite active storms. Always checkavailabilitybefore promising features to your designer.
Interview corner
Junior: “How do you get the current temperature for a user’s location?”
Request
CLLocationWhenInUseUsageDescriptionand use CLLocationManager to get aCLLocation. Add the WeatherKit capability, then callWeatherService.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
criticalinterruption 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.