WeatherKit: Fetching Forecasts with Swift's Native Weather API


Your weather feature works in development, then a user in Tokyo opens the app and you realize the third-party API returns Fahrenheit with no locale awareness, rate-limits you at 60 requests per hour, and just raised its pricing tier. Apple’s WeatherKit eliminates all three problems: it returns localized data, offers 500,000 calls per month on the free tier, and runs through the same privacy-first infrastructure as the built-in Weather app — no user tracking, no API keys embedded in your binary.

This post covers WeatherService, the request lifecycle from location to forecast, handling attribution requirements, and patterns for caching weather data efficiently. We will not cover building a full weather UI — that is covered in Build a Weather App. This post assumes you are comfortable with async/await and Core Location’s modern API.

Note: WeatherKit requires iOS 16+ / macOS 13+ and a paid Apple Developer Program membership. The framework is free to use, but your app must have the WeatherKit entitlement enabled in App Store Connect.

Contents

The Problem

Imagine you are building a location-aware app for Pixar’s park operations team. Each theme park — Radiator Springs, Monstropolis, the Great Barrier Reef — needs a live weather dashboard so staff can decide whether to close outdoor attractions. The naive approach is to poll a third-party weather API on a timer:

// The pre-WeatherKit approach — fragile and privacy-invasive
final class LegacyWeatherFetcher {
    private let apiKey = "sk_live_EXPOSED_IN_BINARY" // Decompilable
    private let session = URLSession.shared

    func fetchForecast(lat: Double, lon: Double) async throws -> [String: Any] {
        let url = URL(string: "https://api.thirdparty.com/forecast?lat=\(lat)&lon=\(lon)&key=\(apiKey)")!
        let (data, response) = try await session.data(from: url)
        guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
            throw WeatherError.badResponse
        }
        // Manual JSON parsing, no type safety, units depend on query params
        return try JSONSerialization.jsonObject(with: data) as! [String: Any]
    }
}

This approach has real production costs. The API key ships in the binary and can be extracted with strings. The response format is untyped, so a backend schema change silently breaks your app. Temperature units depend on a query parameter you need to remember to set per locale. And you are sending user coordinates to a third party with no privacy nutrition label coverage.

WeatherKit solves all of this. The request goes through Apple’s servers using your app’s entitlement — no API key in the binary. The response is fully typed Swift structs with Measurement<UnitTemperature> values that respect the user’s locale. And Apple’s privacy infrastructure means weather requests are not associated with the user’s Apple ID.

Setting Up WeatherKit

Before writing any code, you need to enable the WeatherKit capability in two places.

App Store Connect:

  1. Sign in to App Store Connect and navigate to your app’s identifier.
  2. Under Capabilities, enable WeatherKit.

Xcode project:

  1. Select your target, go to Signing & Capabilities.
  2. Click + Capability and add WeatherKit.

This adds the com.apple.developer.weatherkit entitlement to your app. Without it, every WeatherService call throws a WeatherError with domain CKError — a confusing error that does not mention WeatherKit at all.

Warning: The WeatherKit entitlement requires a paid Apple Developer Program account. If you are using a free provisioning profile, the capability will not appear in Xcode’s capability picker. There is no simulator workaround — you need the real entitlement.

Fetching Weather Data

Apple Docs: WeatherService — WeatherKit

The entry point is WeatherService.shared. You pass a CLLocation and specify which data sets you want. Here is a production-ready service for our Pixar park operations dashboard:

import WeatherKit
import CoreLocation

@available(iOS 16, *)
actor ParkWeatherService {
    private let weatherService = WeatherService.shared

    /// Fetches current conditions and hourly forecast for a park location.
    func fetchParkForecast(
        for location: CLLocation
    ) async throws -> ParkForecast {
        let weather = try await weatherService.weather(
            for: location,
            including: .current, .hourly
        )

        return ParkForecast(
            current: weather.0,
            hourly: weather.1
        )
    }
}

struct ParkForecast: Sendable {
    let current: CurrentWeather
    let hourly: Forecast<HourWeather>
}

The weather(for:including:) method is generic over its including parameters. You can request any combination of data sets by passing multiple WeatherQuery values. The return type is a tuple whose elements match the queries you passed:

// Request current + daily + minute-by-minute
let weather = try await weatherService.weather(
    for: location,
    including: .current, .daily, .minute
)
// weather.0 is CurrentWeather
// weather.1 is Forecast<DayWeather>
// weather.2 is Forecast<MinuteWeather>?  — minute forecasts are optional (US only)

Note: Minute-by-minute precipitation forecasts (.minute) are only available in the United States, United Kingdom, Ireland, and Australia as of iOS 18. For other regions, the value is nil. Always handle this optionality.

Reading Current Conditions

CurrentWeather provides strongly typed properties for every condition you would expect. All temperature values are Measurement<UnitTemperature>, so they respect the user’s locale without any conversion logic on your part:

func displayCurrentConditions(_ current: CurrentWeather) {
    let formatter = MeasurementFormatter()
    formatter.unitOptions = .providedUnit

    let temp = formatter.string(from: current.temperature)
    let feelsLike = formatter.string(from: current.apparentTemperature)
    let condition = current.condition.description
    let uvIndex = current.uvIndex.value

    print("Radiator Springs: \(temp) (feels like \(feelsLike))")
    print("Condition: \(condition), UV Index: \(uvIndex)")
    print("Humidity: \(Int(current.humidity * 100))%")
    print("Wind: \(formatter.string(from: current.wind.speed)) \(current.wind.compassDirection)")
}
Radiator Springs: 28 C (feels like 31 C)
Condition: Mostly Clear, UV Index: 7
Humidity: 45%
Wind: 12 km/h S

Iterating Over Forecasts

Forecast<HourWeather> conforms to RandomAccessCollection, so you can use it directly in SwiftUI ForEach or filter it in plain Swift:

func hoursAboveThreshold(
    _ forecast: Forecast<HourWeather>,
    threshold: Measurement<UnitTemperature>
) -> [HourWeather] {
    forecast.filter { hour in
        hour.temperature > threshold
    }
}

// Find hours where Monstropolis temperature exceeds 35 C
let hotHours = hoursAboveThreshold(
    parkForecast.hourly,
    threshold: Measurement(value: 35, unit: .celsius)
)

for hour in hotHours {
    print("\(hour.date.formatted(.dateTime.hour())): \(hour.temperature)")
}

Daily Forecasts and Precipitation

Daily forecasts are particularly useful for week-ahead planning. Each DayWeather includes high/low temperatures, precipitation probability, and snowfall amounts:

func fetchWeeklyOutlook(for location: CLLocation) async throws {
    let daily = try await WeatherService.shared.weather(
        for: location,
        including: .daily
    )

    for day in daily.prefix(7) {
        let high = day.highTemperature
        let low = day.lowTemperature
        let rainChance = Int(day.precipitationChance * 100)

        print("\(day.date.formatted(.dateTime.weekday(.wide))): " +
              "\(low.formatted()) - \(high.formatted()), " +
              "\(rainChance)% chance of \(day.precipitation.description)")
    }
}

Advanced Usage

Weather Alerts

WeatherAlertCollection provides severe weather alerts issued by government meteorological agencies. This is critical for any app making safety-relevant decisions:

@available(iOS 16, *)
func checkSevereAlerts(
    for location: CLLocation
) async throws -> [WeatherAlert] {
    let alerts = try await WeatherService.shared.weather(
        for: location,
        including: .alerts
    )

    // Filter for actionable severity levels
    guard let alerts else { return [] }

    let severeAlerts = alerts.filter { alert in
        alert.severity == .extreme || alert.severity == .severe
    }

    for alert in severeAlerts {
        print("ALERT for Radiator Springs: \(alert.summary)")
        print("Source: \(alert.source)")
        print("Effective: \(alert.effectiveDate.formatted())")
        if let expires = alert.expiresDate {
            print("Expires: \(expires.formatted())")
        }
    }

    return severeAlerts
}

Warning: Weather alerts are not available in all countries. The alerts query returns nil for unsupported regions. Do not force-unwrap this value.

Attribution Requirements

Apple requires you to display a WeatherKit attribution in any UI that shows weather data. This is not optional — App Review will reject your app without it.

@available(iOS 16, *)
struct WeatherAttributionView: View {
    @State private var attribution: WeatherAttribution?

    var body: some View {
        VStack {
            if let attribution {
                AsyncImage(url: colorScheme == .light
                    ? attribution.combinedMarkLightURL
                    : attribution.combinedMarkDarkURL
                ) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    EmptyView()
                }
                .frame(height: 12)

                Link("Weather data", destination: attribution.legalPageURL)
                    .font(.caption2)
            }
        }
        .task {
            attribution = try? await WeatherService.shared.attribution
        }
    }

    @Environment(\.colorScheme) private var colorScheme
}

Apple Docs: WeatherAttribution — WeatherKit

Combining WeatherKit with Core Location

In production, you typically pair WeatherKit with a location manager. Here is a pattern that fetches weather for the user’s current location using Core Location’s modern async API:

@available(iOS 17, *)
actor LocationWeatherCoordinator {
    private let weatherService = WeatherService.shared

    func fetchWeatherForCurrentLocation() async throws -> CurrentWeather {
        let updates = CLLocationUpdate.liveUpdates()

        for await update in updates {
            guard let location = update.location else { continue }

            // We only need one fix — break after the first valid location
            let weather = try await weatherService.weather(
                for: location,
                including: .current
            )
            return weather
        }

        throw CoordinatorError.noLocationAvailable
    }

    enum CoordinatorError: Error {
        case noLocationAvailable
    }
}

Caching with TimeInterval Checks

WeatherKit does not provide built-in caching, but the metadata property on each weather response tells you when the data was fetched and when it expires:

@available(iOS 16, *)
actor CachedWeatherService {
    private let weatherService = WeatherService.shared
    private var cachedForecast: ParkForecast?
    private var lastFetchDate: Date?
    private let minimumRefreshInterval: TimeInterval = 600 // 10 minutes

    func forecast(for location: CLLocation) async throws -> ParkForecast {
        if let cached = cachedForecast,
           let lastFetch = lastFetchDate,
           Date.now.timeIntervalSince(lastFetch) < minimumRefreshInterval {
            return cached
        }

        let weather = try await weatherService.weather(
            for: location,
            including: .current, .hourly
        )

        let forecast = ParkForecast(
            current: weather.0,
            hourly: weather.1
        )

        cachedForecast = forecast
        lastFetchDate = .now

        return forecast
    }
}

Performance Considerations

API call limits. The free tier grants 500,000 weather(for:including:) calls per month across all users of your app. Each data set in a single call counts as one request. So .current, .daily, .hourly in one call costs 3 API calls against your quota. Batch your data set requests into as few invocations as possible — one call with three data sets is cheaper in latency than three separate calls, even though the API count is the same.

Network latency. Each call to weather(for:including:) is a network request to Apple’s servers. Expect 200-800ms depending on region and network conditions. Never call it on every body evaluation — fetch once in a .task modifier or on a timer and store the result in your model layer.

Memory. Forecast<HourWeather> for a 10-day hourly forecast contains approximately 240 HourWeather values. Each value is a lightweight struct, so the total memory footprint is modest — typically under 50KB. You can safely hold multiple forecasts in memory for a multi-location app like our park operations dashboard.

Background refresh. For apps that need fresh weather data even when backgrounded (widgets, Live Activities), use BGAppRefreshTask to schedule periodic fetches. WeatherKit calls work in background execution contexts as long as the entitlement is present.

Apple Docs: WeatherService — WeatherKit

When to Use (and When Not To)

ScenarioRecommendation
Current conditions, hourly, or daily forecastsUse WeatherKit. Simplest path with no API key management.
App targets iOS 15 or earlierWeatherKit requires iOS 16. Use a third-party API.
Historical weather data neededWeatherKit has no historical data. Use Open-Meteo or similar.
Hyper-local radar imageryWeatherKit has no radar tiles. Pair with a radar tile provider.
More than 500K calls/monthPurchase capacity in App Store Connect ($0.99 per 500K extra).
Cross-platform (Android + iOS)Use WeatherKit’s REST API with JWT for your backend.
Widget or Live ActivityWeatherKit works in extension contexts. Pair with background refresh.

Summary

  • WeatherService.shared.weather(for:including:) is a single async call that returns strongly typed, locale-aware weather data with no API keys in your binary.
  • Request multiple data sets (.current, .daily, .hourly, .minute, .alerts) in a single call to minimize latency. Each data set counts as one API call against the 500K monthly free tier.
  • Minute-by-minute precipitation is limited to select countries — always treat the .minute result as optional.
  • Weather alerts require nil handling since they are unavailable in many regions.
  • Display the required WeatherAttribution in your UI or risk App Review rejection.
  • Cache responses with a reasonable refresh interval (10-15 minutes) to stay within API limits and reduce network overhead.

For a hands-on walkthrough of building a complete weather UI with WeatherKit, see Build a Weather App. To display weather data on an interactive map, pair this with MapKit for SwiftUI.