
Weatherkit
- 2.5k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
weatherkit integrates Apple WeatherKit forecasts, alerts, selective queries, attribution, and caching in Swift iOS apps.
About
The weatherkit skill guides WeatherService integration for Swift 6.3 and iOS 26+ apps. Setup requires WeatherKit capability in Xcode, App ID enablement, Apple Developer Program membership, and location usage strings when using device location. Fetch current conditions, 25-hour hourly, 10-day daily, minute precipitation, alerts, and iOS 18+ changes and historicalComparisons via selective WeatherQuery including arguments to minimize quota usage. Temperatures are Measurement values displayed with formatted() for locale units. Minute forecasts and some alerts return optional nil in unsupported regions; never force-unwrap. Apple Weather attribution is legally required: fetch attribution, show combined mark URLs for light or dark schemes, and link legalPageURL beside all weather data. Cache responses until metadata.expirationDate instead of refetching on every onAppear; use an actor or model loadIfNeeded pattern. WeatherAvailability reports only alert and minute availability, not broad dataset support. Custom date ranges use inclusive start and exclusive end with historical data from August 2021 and up to 10 future days. Statistics and summaries use dedicated WeatherService methods, not.
- WeatherService selective .current, .hourly, .daily, .minute, .alerts queries.
- Required Apple Weather attribution marks and legalPageURL beside all data.
- Cache until metadata.expirationDate; avoid unconditional onAppear fetches.
- Minute forecast and alerts optional; handle nil for unsupported regions.
- iOS 18+ .changes and .historicalComparisons for contextual weather features.
Weatherkit by the numbers
- 2,517 all-time installs (skills.sh)
- +100 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #86 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
weatherkit capabilities & compatibility
- Capabilities
- selective weatherquery dataset fetching · swiftui attribution display patterns · expiration based response caching · optional minute and alert availability handling · ios 18 context queries for changes and compariso
- Use cases
- frontend · api development · ui design
What weatherkit says it does
Apple requires apps using WeatherKit to display attribution. This is a legal requirement.
Each dataset query counts against your API quota. Fetch only what you display.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill weatherkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.5k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I fetch and display WeatherKit data correctly with quotas, attribution, and regional availability handled?
Integrate Apple WeatherKit for current weather, forecasts, alerts, selective queries, attribution, caching, and iOS 18+ context features in Swift apps.
Who is it for?
iOS apps adding current weather, forecasts, alerts, or contextual weather with WeatherKit entitlements.
Skip if: Skip for non-Apple platforms or weather UIs that do not use WeatherService APIs.
When should I use this skill?
User integrates WeatherKit, shows forecasts or alerts, needs attribution, or reviews WeatherKit query limits.
What you get
Working weather fetches with selective queries, locale formatting, legal attribution, and expiration-based caching.
- WeatherManager implementation
- SwiftUI weather views
Files
WeatherKit
Fetch current conditions, hourly and daily forecasts, weather alerts, and historical statistics using WeatherService. Display required Apple Weather attribution. Targets Swift 6.3 / iOS 26+.
Contents
- Setup
- Fetching Current Weather
- Forecasts
- Weather Alerts
- Selective Queries
- Context Queries
- Attribution
- Availability
- Common Mistakes
- Review Checklist
- References
Setup
Project Configuration
1. Enable the WeatherKit capability in Xcode (adds the entitlement) 2. Enable WeatherKit for your App ID in the Apple Developer portal 3. Add NSLocationWhenInUseUsageDescription to Info.plist if using device location 4. WeatherKit requires an active Apple Developer Program membership
Import
import WeatherKit
import CoreLocationCreating the Service
Use the shared singleton or create an instance. WeatherService conforms to Sendable; keep app cache and UI state isolated separately.
let weatherService = WeatherService.shared
// or
let weatherService = WeatherService()Fetching Current Weather
Fetch current conditions for a location. Returns a Weather object with all available datasets.
WeatherKit temperatures are Measurement<UnitTemperature> values; display them with .formatted() so units and number formatting follow the user's locale.
func fetchCurrentWeather(for location: CLLocation) async throws -> CurrentWeather {
let weather = try await weatherService.weather(for: location)
return weather.currentWeather
}
// Using the result
func displayCurrent(_ current: CurrentWeather) {
let temp = current.temperature // Measurement<UnitTemperature>
let condition = current.condition // WeatherCondition enum
let symbol = current.symbolName // SF Symbol name
let humidity = current.humidity // Double (0-1)
let wind = current.wind // Wind (speed, direction, gust)
let uvIndex = current.uvIndex // UVIndex
print("\(condition): \(temp.formatted())")
}Forecasts
Hourly Forecast
Returns 25 contiguous hours starting from the current hour by default.
func fetchHourlyForecast(for location: CLLocation) async throws -> Forecast<HourWeather> {
let weather = try await weatherService.weather(for: location)
return weather.hourlyForecast
}
// Iterate hours
for hour in hourlyForecast {
print("\(hour.date): \(hour.temperature.formatted()), \(hour.condition)")
}Daily Forecast
Returns 10 contiguous days starting from the current day by default.
func fetchDailyForecast(for location: CLLocation) async throws -> Forecast<DayWeather> {
let weather = try await weatherService.weather(for: location)
return weather.dailyForecast
}
// Iterate days
for day in dailyForecast {
print("\(day.date): \(day.lowTemperature.formatted()) - \(day.highTemperature.formatted())")
print(" Condition: \(day.condition), Precipitation: \(day.precipitationChance)")
}Custom Date Range
Request forecasts for specific date ranges using WeatherQuery.
Daily and hourly date-range queries use an inclusive startDate and exclusive endDate. They can include historical data from August 1, 2021. Forecasts are available up to 10 days in the future; each request returns at most 10 daily forecast days or about 240 hourly forecast hours.
func fetchExtendedForecast(for location: CLLocation) async throws -> Forecast<DayWeather> {
let startDate = Date.now
let endDate = Calendar.current.date(byAdding: .day, value: 10, to: startDate)!
let forecast = try await weatherService.weather(
for: location,
including: .daily(startDate: startDate, endDate: endDate)
)
return forecast
}For tomorrow-specific guidance, request the local tomorrow day interval rather than using minute forecasts:
func fetchTomorrowForecast(for location: CLLocation) async throws -> Forecast<DayWeather> {
let calendar = Calendar.current
let tomorrow = calendar.startOfDay(
for: calendar.date(byAdding: .day, value: 1, to: .now)!
)
let dayAfterTomorrow = calendar.date(byAdding: .day, value: 1, to: tomorrow)!
return try await weatherService.weather(
for: location,
including: .daily(startDate: tomorrow, endDate: dayAfterTomorrow)
)
}Weather Alerts
Fetch active weather alerts for a location. Alerts include severity, summary, and affected regions.
func fetchAlerts(for location: CLLocation) async throws -> [WeatherAlert]? {
let weather = try await weatherService.weather(for: location)
return weather.weatherAlerts
}
// Process alerts
if let alerts = weatherAlerts {
for alert in alerts {
print("Alert: \(alert.summary)")
print("Severity: \(alert.severity)")
print("Region: \(alert.region ?? "Unknown region")")
print("Details: \(alert.detailsURL)") // Non-optional and required for attribution
}
}For alert dashboards, name WeatherAvailability explicitly when discussing support checks: it exposes alertAvailability and minuteAvailability only, not a broad availability matrix for current, hourly, or daily weather.
Selective Queries
Fetch only the datasets you need to minimize API usage and response size. Each WeatherQuery type maps to one dataset.
Single Dataset
let current = try await weatherService.weather(
for: location,
including: .current
)
// current is CurrentWeatherMultiple Datasets
let (current, hourly, daily) = try await weatherService.weather(
for: location,
including: .current, .hourly, .daily
)
// current: CurrentWeather, hourly: Forecast<HourWeather>, daily: Forecast<DayWeather>Minute Forecast
Available in limited regions. Returns precipitation forecasts at minute granularity for the next hour.
let minuteForecast = try await weatherService.weather(
for: location,
including: .minute
)
// minuteForecast: Forecast<MinuteWeather>? (nil if unavailable)Available Query Types
| Query | Return Type | Description |
|---|---|---|
.current | CurrentWeather | Current observed conditions |
.hourly | Forecast<HourWeather> | 25 hours from current hour |
.daily | Forecast<DayWeather> | 10 days from today |
.minute | Forecast<MinuteWeather>? | Next-hour precipitation (limited regions) |
.alerts | [WeatherAlert]? | Active weather alerts |
.availability | WeatherAvailability | Alert and minute forecast availability only |
.changes | WeatherChanges? | Significant upcoming weather changes (iOS 18+) |
.historicalComparisons | HistoricalComparisons? | Current weather compared to historical averages (iOS 18+) |
Dashboard Review Checklist
For a current-temperature and alert dashboard review, explicitly cover:
- Selective
.current, .alertsqueries instead ofweather(for:)for every dataset - No unconditional
onAppear/.tasknetwork fetch; use model or cacheloadIfNeeded WeatherMetadata.expirationDatecache freshnessWeatherService.shared.attribution, mark URLs, andlegalPageURLbeside weather data- Optional
alert.region, non-optionalalert.detailsURL, and alert detail links WeatherAvailabilityonly foralertAvailabilityandminuteAvailabilityMeasurement<UnitTemperature>.formatted()for displayed temperatures- WeatherKit capability/App ID setup and location permission when using device location
Context Queries
Use the iOS 18+ context queries when the app needs to explain why today's weather matters, not just display raw forecast values. Both query results are optional.
For "unusual tomorrow" or "what is changing?" features, request both .changes and .historicalComparisons. Use .changes for significant upcoming changes, then use .historicalComparisons to explain how current or forecast conditions compare with historical averages.
let (changes, comparisons) = try await weatherService.weather(
for: location,
including: .changes, .historicalComparisons
)For historical statistics, use the WeatherService statistics and summary methods rather than WeatherQuery. In variadic including: calls, state that tuple result order matches the query argument order. Load references/weatherkit-patterns.md when implementing daily summaries, daily statistics, hourly statistics, or monthly statistics. Use statistics properties such as averagePrecipitationProbability, not forecast-only DayWeather.precipitationChance, in statistics examples.
Attribution
Apple requires apps using WeatherKit to display attribution. This is a legal requirement.
Fetching Attribution
func fetchAttribution() async throws -> WeatherAttribution {
return try await weatherService.attribution
}Displaying Attribution in SwiftUI
import SwiftUI
import WeatherKit
struct WeatherAttributionView: View {
let attribution: WeatherAttribution
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack {
// Display the Apple Weather mark
AsyncImage(url: markURL) { image in
image
.resizable()
.scaledToFit()
.frame(height: 20)
} placeholder: {
EmptyView()
}
// Link to the legal attribution page
Link("Weather data sources", destination: attribution.legalPageURL)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
private var markURL: URL {
colorScheme == .dark
? attribution.combinedMarkDarkURL
: attribution.combinedMarkLightURL
}
}Attribution Properties
| Property | Use |
|---|---|
combinedMarkLightURL | Apple Weather mark for light backgrounds |
combinedMarkDarkURL | Apple Weather mark for dark backgrounds |
squareMarkURL | Square Apple Weather logo |
legalPageURL | URL to the legal attribution web page |
legalAttributionText | Text alternative when a web view is not feasible |
serviceName | Weather data provider name |
Availability
Check whether weather alerts or minute forecast data are available for a location. WeatherAvailability reports only alert and minute availability; other datasets, such as current weather, are expected to be supported for geographic locations.
func checkAvailability(for location: CLLocation) async throws {
let availability = try await weatherService.weather(
for: location,
including: .availability
)
// Check specific dataset availability
if availability.alertAvailability == .available {
// Safe to fetch alerts
}
if availability.minuteAvailability == .available {
// Minute forecast available for this region
}
}Common Mistakes
DON'T: Ship without Apple Weather attribution
Omitting attribution violates the WeatherKit terms of service and risks App Review rejection.
// WRONG: Show weather data without attribution
VStack {
Text("72F, Sunny")
}
// CORRECT: Always include attribution
VStack {
Text("72F, Sunny")
WeatherAttributionView(attribution: attribution)
}DON'T: Fetch all datasets when you only need current conditions
Each dataset query counts against your API quota. Fetch only what you display.
// WRONG: Fetches everything
let weather = try await weatherService.weather(for: location)
let temp = weather.currentWeather.temperature
// CORRECT: Fetch only current conditions
let current = try await weatherService.weather(
for: location,
including: .current
)
let temp = current.temperatureDON'T: Ignore minute forecast unavailability
Minute forecasts return nil in unsupported regions. Force-unwrapping crashes.
// WRONG: Force-unwrap minute forecast
let minutes = try await weatherService.weather(for: location, including: .minute)
for m in minutes! { ... } // Crash in unsupported regions
// CORRECT: Handle nil
if let minutes = try await weatherService.weather(for: location, including: .minute) {
for m in minutes { ... }
} else {
// Minute forecast not available for this region
}DON'T: Forget the WeatherKit entitlement
Without the capability enabled, WeatherService calls throw at runtime.
// WRONG: No WeatherKit capability configured
let weather = try await weatherService.weather(for: location) // Throws
// CORRECT: Enable WeatherKit in Xcode Signing & Capabilities
// and in the Apple Developer portal for your App IDDON'T: Make repeated requests without caching
WeatherKit models include metadata.expirationDate. Cache responses until that expiration instead of inventing a fixed refresh interval. Avoid unconditional network calls from every onAppear or .task; let an @Observable model, view model, or cache own loadIfNeeded, and reserve explicit refresh for user refresh actions or location/query changes.
// WRONG: Fetch on every view appearance
.task {
let weather = try? await fetchWeather()
}
// CORRECT: let the model/cache decide whether a fetch is needed
actor WeatherCache {
private var cached: CurrentWeather?
private var expiresAt: Date?
func current(for location: CLLocation) async throws -> CurrentWeather {
if let cached, let expiresAt, Date.now < expiresAt {
return cached
}
let fresh = try await WeatherService.shared.weather(
for: location, including: .current
)
cached = fresh
expiresAt = fresh.metadata.expirationDate
return fresh
}
}Review Checklist
- [ ] WeatherKit capability enabled in Xcode and Apple Developer portal
- [ ] Active Apple Developer Program membership (required for WeatherKit)
- [ ] Apple Weather attribution displayed wherever weather data appears
- [ ] Attribution mark uses correct color scheme variant (light/dark)
- [ ] Legal attribution page linked or
legalAttributionTextdisplayed - [ ] Only needed
WeatherQuerydatasets fetched (not fullweather(for:)when unnecessary) - [ ] Minute forecast handled as optional (nil in unsupported regions)
- [ ] Weather alerts checked for nil before iteration
- [ ] Alert detail links use non-optional
detailsURL; optionalregionis nil-safe - [ ] Responses cached until each model's
metadata.expirationDate - [ ]
WeatherAvailabilityused for alert/minute availability, not as a broad support matrix - [ ] Location permission requested before passing
CLLocationto service - [ ] Temperature and measurements formatted with
Measurement.formatted()for locale
References
- Extended patterns (SwiftUI dashboard, charts integration, historical statistics): references/weatherkit-patterns.md
- WeatherKit framework
- WeatherService
- WeatherAttribution
- WeatherQuery
- WeatherQuery.daily(startDate:endDate:))
- WeatherQuery.hourly(startDate:endDate:))
- CurrentWeather
- CurrentWeather.temperature
- Measurement.formatted())
- Forecast
- HourWeather
- DayWeather
- WeatherAlert
- WeatherAvailability
- WeatherMetadata.expirationDate
- WeatherQuery.changes
- WeatherQuery.historicalComparisons
- WeatherKit updates
- Bring context to today's weather
- Fetching weather forecasts with WeatherKit
{
"skill_name": "weatherkit",
"evals": [
{
"id": 0,
"name": "forecast-dashboard-review",
"prompt": "Review this WeatherKit dashboard plan for an iOS app: fetch every dataset with `weather(for:)` on every view appearance, cache for exactly 10 minutes, show current temperature and alert summaries, string-interpolate `current.temperature`, print `alert.region`, skip alert detail links, treat WeatherAvailability as a broad support matrix for all datasets, and put attribution on a separate settings screen. What should change?",
"expected_output": "Flags the WeatherKit correctness issues: fetch only displayed datasets, cache against model metadata expiration dates, make alert region nil-safe, link each alert's non-optional detailsURL, display Apple Weather attribution alongside weather data, and use WeatherAvailability only for alerts and minute forecasts.",
"files": [],
"expectations": [
"The output recommends selective WeatherQuery requests instead of fetching all datasets with weather(for:) for a narrow dashboard.",
"The output rejects unconditional network fetches from every onAppear or .task and recommends a model, cache, loadIfNeeded, or explicit refresh boundary.",
"The output says cache expiry should use WeatherMetadata.expirationDate rather than a fixed 10-minute interval.",
"The output states WeatherAlert.region is optional and should be handled nil-safely.",
"The output states WeatherAlert.detailsURL is non-optional and should be linked for alert details or attribution.",
"The output requires Apple Weather attribution to appear wherever weather data is displayed, not only in a separate settings screen.",
"The output limits WeatherAvailability to alert and minute forecast availability rather than treating it as a broad dataset support matrix.",
"The output recommends Measurement.formatted() or equivalent locale-aware Measurement formatting for displayed temperatures."
]
},
{
"id": 1,
"name": "historical-statistics-modernization",
"prompt": "Modernize this WeatherKit statistics snippet for iOS 18+: `let stats = try await service.dailyStatistics(for: location, forDaysIn: range, including: [.temperature, .precipitation]); for day in stats { print(day.statistics(for: .temperature)?.mean) }`. Include a monthly example too.",
"expected_output": "Replaces the stale array-style statistics code with iOS 18+ variadic APIs where `including: .precipitation, .temperature` returns typed tuple values in the same order. It iterates DailyWeatherStatistics and MonthlyWeatherStatistics collections and uses documented properties such as averageHighTemperature, averageLowTemperature, averagePrecipitationAmount, day, and month.",
"files": [],
"expectations": [
"The output does not use `including: [.temperature, .precipitation]` for WeatherKit statistics queries.",
"The output does not call a nonexistent or stale `statistics(for:)` helper on returned statistics elements.",
"The output explicitly states that variadic statistics tuple result order matches the `including:` query argument order.",
"The output uses documented daily statistics properties such as `day`, `averageHighTemperature`, `averageLowTemperature`, or `averagePrecipitationAmount`.",
"The output shows or describes monthlyStatistics returning typed MonthlyWeatherStatistics collections and uses documented properties such as `month`, `averageLowTemperature`, or `averagePrecipitationAmount`.",
"The output gates statistics APIs with iOS 18+ availability or explicitly states they require iOS 18+."
]
},
{
"id": 2,
"name": "context-queries-and-ranges",
"prompt": "I want a WeatherKit feature that explains whether tomorrow's weather is unusual, can look back at forecast history, and avoids unsupported minute forecasts. Which queries should I use and what range or availability constraints should the code enforce?",
"expected_output": "Uses iOS 18+ `.changes` and `.historicalComparisons` optional queries for context, daily/hourly date-range queries for historical forecast data from August 1, 2021, enforces future and per-request limits, and checks WeatherAvailability before minute forecasts.",
"files": [],
"expectations": [
"The output recommends `.changes` for significant upcoming weather changes and notes the result is optional.",
"The output recommends `.historicalComparisons` for comparing current weather to historical averages and notes the result is optional.",
"The output states `.changes` and `.historicalComparisons` require iOS 18+ or equivalent platform availability.",
"The output shows or describes fetching tomorrow with a dated `.daily(startDate:endDate:)` day interval rather than minute forecasts.",
"The output states daily and hourly range queries can access historical data from August 1, 2021.",
"The output states forecast range requests are limited to 10 days in the future and at most 10 daily days or about 240 hourly hours per request.",
"The output states WeatherKit date-range query startDate is inclusive and endDate is exclusive.",
"The output checks WeatherAvailability before relying on minute forecast data and handles a nil minute forecast."
]
}
]
}
WeatherKit Extended Patterns
Overflow reference for the weatherkit skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- WeatherKit SwiftUI Integration
- Charts Integration
- Historical Weather Statistics
- Weather Changes and Historical Comparisons
- Weather Condition Mapping
- Caching Strategy
- Location-Based Weather
- References
WeatherKit SwiftUI Integration
SwiftUI views may trigger loads from .task or .refreshable, but the @Observable model should decide whether a network request is needed. Use loadWeatherIfNeeded for automatic view lifecycle loads and a separate refresh path for explicit user refresh.
Weather Manager with @Observable
import WeatherKit
import CoreLocation
@Observable
@MainActor
final class WeatherManager {
private let service = WeatherService.shared
var current: CurrentWeather?
var hourlyForecast: Forecast<HourWeather>?
var dailyForecast: Forecast<DayWeather>?
var alerts: [WeatherAlert]?
var attribution: WeatherAttribution?
var isLoading = false
var error: Error?
func loadWeatherIfNeeded(for location: CLLocation) async {
guard current == nil else { return }
await refreshWeather(for: location)
}
func refreshWeather(for location: CLLocation) async {
isLoading = true
error = nil
do {
let (current, hourly, daily, alerts) = try await service.weather(
for: location,
including: .current, .hourly, .daily, .alerts
)
self.current = current
self.hourlyForecast = hourly
self.dailyForecast = daily
self.alerts = alerts
self.attribution = try await service.attribution
} catch {
self.error = error
}
isLoading = false
}
}Weather Dashboard View
import SwiftUI
import WeatherKit
struct WeatherDashboardView: View {
@Environment(WeatherManager.self) private var manager
let location: CLLocation
var body: some View {
NavigationStack {
ScrollView {
VStack {
if manager.isLoading {
ProgressView("Loading weather...")
} else if let current = manager.current {
currentConditionsCard(current)
}
if let hourly = manager.hourlyForecast {
hourlyForecastSection(hourly)
}
if let daily = manager.dailyForecast {
dailyForecastSection(daily)
}
if let alerts = manager.alerts, !alerts.isEmpty {
alertsSection(alerts)
}
if let attribution = manager.attribution {
WeatherAttributionView(attribution: attribution)
}
}
.padding()
}
.navigationTitle("Weather")
.task {
await manager.loadWeatherIfNeeded(for: location)
}
.refreshable {
await manager.refreshWeather(for: location)
}
}
}
private func currentConditionsCard(_ current: CurrentWeather) -> some View {
VStack {
Image(systemName: current.symbolName)
.font(.system(size: 60))
.symbolRenderingMode(.multicolor)
Text(current.temperature.formatted())
.font(.system(size: 48, weight: .thin))
Text(current.condition.description)
.font(.title3)
.foregroundStyle(.secondary)
HStack {
Label(
"Humidity \(current.humidity.formatted(.percent))",
systemImage: "humidity"
)
Label(
"Wind \(current.wind.speed.formatted())",
systemImage: "wind"
)
Label(
"UV \(current.uvIndex.value)",
systemImage: "sun.max"
)
}
.font(.caption)
}
.padding()
}
private func hourlyForecastSection(_ forecast: Forecast<HourWeather>) -> some View {
VStack(alignment: .leading) {
Text("Hourly Forecast")
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Array(forecast.prefix(12)), id: \.date) { hour in
VStack {
Text(hour.date, format: .dateTime.hour())
.font(.caption)
Image(systemName: hour.symbolName)
.symbolRenderingMode(.multicolor)
Text(hour.temperature.formatted())
.font(.subheadline)
}
}
}
}
}
}
private func dailyForecastSection(_ forecast: Forecast<DayWeather>) -> some View {
VStack(alignment: .leading) {
Text("10-Day Forecast")
.font(.headline)
ForEach(Array(forecast), id: \.date) { day in
HStack {
Text(day.date, format: .dateTime.weekday(.abbreviated))
.frame(width: 40, alignment: .leading)
Image(systemName: day.symbolName)
.symbolRenderingMode(.multicolor)
.frame(width: 30)
Text(day.lowTemperature.formatted())
.foregroundStyle(.secondary)
.frame(width: 50, alignment: .trailing)
temperatureBar(low: day.lowTemperature, high: day.highTemperature)
Text(day.highTemperature.formatted())
.frame(width: 50)
}
.font(.subheadline)
}
}
}
private func temperatureBar(
low: Measurement<UnitTemperature>,
high: Measurement<UnitTemperature>
) -> some View {
Capsule()
.fill(.linearGradient(
colors: [.blue, .orange],
startPoint: .leading,
endPoint: .trailing
))
.frame(height: 4)
.containerRelativeFrame(.horizontal) { length, _ in
length * 0.3
}
}
private func alertsSection(_ alerts: [WeatherAlert]) -> some View {
VStack(alignment: .leading) {
Text("Weather Alerts")
.font(.headline)
ForEach(alerts, id: \.detailsURL) { alert in
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(alert.severity == .extreme ? .red : .orange)
VStack(alignment: .leading) {
Text(alert.summary)
.font(.subheadline)
Text(alert.region ?? "Affected area unavailable")
.font(.caption)
.foregroundStyle(.secondary)
Link("Details", destination: alert.detailsURL)
.font(.caption2)
}
}
.padding()
.background(.yellow.opacity(0.1))
.clipShape(.rect(cornerRadius: 8))
}
}
}
}When alert or minute support is uncertain, fetch WeatherAvailability and use only alertAvailability or minuteAvailability; it is not a broad matrix for current, hourly, or daily forecast support.
Attribution View
struct WeatherAttributionView: View {
let attribution: WeatherAttribution
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack {
AsyncImage(url: markURL) { image in
image
.resizable()
.scaledToFit()
.frame(height: 12)
} placeholder: {
Text(attribution.serviceName)
.font(.caption2)
}
Link(destination: attribution.legalPageURL) {
Text("Data Sources")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding(.vertical)
}
private var markURL: URL {
colorScheme == .dark
? attribution.combinedMarkDarkURL
: attribution.combinedMarkLightURL
}
}Charts Integration
Hourly Temperature Chart
import SwiftUI
import Charts
import WeatherKit
struct HourlyTemperatureChart: View {
let forecast: Forecast<HourWeather>
var body: some View {
Chart(Array(forecast.prefix(24)), id: \.date) { hour in
LineMark(
x: .value("Hour", hour.date),
y: .value("Temperature", hour.temperature.converted(to: .celsius).value)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(.orange)
AreaMark(
x: .value("Hour", hour.date),
y: .value("Temperature", hour.temperature.converted(to: .celsius).value)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(.orange.opacity(0.1))
}
.chartYAxisLabel("Temperature (C)")
.chartXAxis {
AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
AxisGridLine()
AxisValueLabel(format: .dateTime.hour())
}
}
.frame(height: 200)
}
}Daily Precipitation Chart
struct DailyPrecipitationChart: View {
let forecast: Forecast<DayWeather>
var body: some View {
Chart(Array(forecast), id: \.date) { day in
BarMark(
x: .value("Day", day.date, unit: .day),
y: .value("Chance", day.precipitationChance)
)
.foregroundStyle(.blue.gradient)
}
.chartYScale(domain: 0...1)
.chartYAxis {
AxisMarks(format: .percent)
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
.frame(height: 150)
}
}Historical Weather Statistics
WeatherKit provides iOS 18+ historical statistics through variadic APIs. The tuple return order matches the including: query order. Use statistics properties such as averagePrecipitationProbability; do not use forecast-only DayWeather.precipitationChance in statistics examples.
Daily Statistics
@available(iOS 18.0, *)
func fetchDailyStats(
for location: CLLocation,
dateRange: DateInterval
) async throws -> [(day: Int, averageHigh: Measurement<UnitTemperature>, averagePrecipitation: Measurement<UnitLength>)] {
let (dailyPrecipitation, dailyTemperature) = try await WeatherService.shared.dailyStatistics(
for: location,
forDaysIn: dateRange,
including: .precipitation, .temperature
)
// Tuple order follows the variadic query order above.
return zip(dailyPrecipitation, dailyTemperature).map { precipitation, temperature in
(
day: temperature.day,
averageHigh: temperature.averageHighTemperature,
averagePrecipitation: precipitation.averagePrecipitationAmount
)
}
}Monthly Statistics
@available(iOS 18.0, *)
func fetchMonthlyStats(
for location: CLLocation
) async throws -> [(month: Int, averageLow: Measurement<UnitTemperature>, averagePrecipitation: Measurement<UnitLength>)] {
let (monthlyPrecipitation, monthlyTemperature) = try await WeatherService.shared.monthlyStatistics(
for: location,
including: .precipitation, .temperature
)
// Tuple order follows the variadic query order above.
return zip(monthlyPrecipitation, monthlyTemperature).map { precipitation, temperature in
(
month: temperature.month,
averageLow: temperature.averageLowTemperature,
averagePrecipitation: precipitation.averagePrecipitationAmount
)
}
}Weather Changes and Historical Comparisons
Use the optional iOS 18+ context queries to summarize why forecast data matters for a user. .changes reports upcoming significant changes; .historicalComparisons compares current conditions to historical averages and returns comparisons ordered by significance.
For "unusual tomorrow" features, combine .changes with the tomorrow daily date-range forecast and .historicalComparisons.
@available(iOS 18.0, *)
func contextHighlights(for location: CLLocation) async throws -> [String] {
let (changes, comparisons) = try await WeatherService.shared.weather(
for: location,
including: .changes, .historicalComparisons
)
var highlights: [String] = []
for change in changes?.changes ?? [] {
switch change.highTemperature {
case .increase:
highlights.append("High temperature is expected to rise.")
case .decrease:
highlights.append("High temperature is expected to fall.")
case .steady:
break
@unknown default:
break
}
}
for comparison in comparisons?.comparisons ?? [] {
switch comparison {
case .highTemperature(let trend):
highlights.append("High temperature is \(trend.deviation).")
case .lowTemperature(let trend):
highlights.append("Low temperature is \(trend.deviation).")
case .precipitationAmount(let trend):
highlights.append("Precipitation is \(trend.deviation).")
case .snowfallAmount(let trend):
highlights.append("Snowfall is \(trend.deviation).")
@unknown default:
break
}
}
return highlights
}Weather Condition Mapping
Mapping Conditions to Colors
extension WeatherCondition {
var themeColor: Color {
switch self {
case .clear, .mostlyClear:
return .yellow
case .partlyCloudy, .mostlyCloudy, .cloudy:
return .gray
case .rain, .heavyRain, .drizzle:
return .blue
case .snow, .heavySnow, .flurries, .sleet, .freezingRain,
.freezingDrizzle, .wintryMix, .blizzard:
return .cyan
case .thunderstorms, .strongStorms, .tropicalStorm, .hurricane:
return .purple
case .foggy, .haze, .smoky:
return .gray.opacity(0.6)
case .breezy, .windy:
return .teal
case .hot:
return .red
case .frigid, .blowingDust:
return .indigo
@unknown default:
return .primary
}
}
}Mapping Severity to Priority
extension WeatherSeverity {
var displayPriority: Int {
switch self {
case .extreme:
return 4
case .severe:
return 3
case .moderate:
return 2
case .minor:
return 1
case .unknown:
return 0
@unknown default:
return 0
}
}
}Caching Strategy
Actor-Based Weather Cache
actor WeatherCache {
struct CacheEntry {
let weather: CurrentWeather
let hourly: Forecast<HourWeather>
let daily: Forecast<DayWeather>
let expiresAt: Date
}
private var cache: [String: CacheEntry] = [:]
func get(for key: String) -> CacheEntry? {
guard let entry = cache[key], Date.now < entry.expiresAt else {
cache[key] = nil
return nil
}
return entry
}
func set(_ entry: CacheEntry, for key: String) {
cache[key] = entry
}
/// Generate a cache key from a location (rounded to ~1km precision)
static func key(for location: CLLocation) -> String {
let lat = (location.coordinate.latitude * 100).rounded() / 100
let lon = (location.coordinate.longitude * 100).rounded() / 100
return "\(lat),\(lon)"
}
}Using the Cache
@Observable
@MainActor
final class CachedWeatherManager {
private let service = WeatherService.shared
private let cache = WeatherCache()
var current: CurrentWeather?
func fetchWeather(for location: CLLocation) async throws {
let key = WeatherCache.key(for: location)
if let cached = await cache.get(for: key) {
current = cached.weather
return
}
let (current, hourly, daily) = try await service.weather(
for: location,
including: .current, .hourly, .daily
)
let entry = WeatherCache.CacheEntry(
weather: current,
hourly: hourly,
daily: daily,
expiresAt: min(
current.metadata.expirationDate,
hourly.metadata.expirationDate,
daily.metadata.expirationDate
)
)
await cache.set(entry, for: key)
self.current = current
}
}Location-Based Weather
Combining CoreLocation with WeatherKit
import CoreLocation
import WeatherKit
@Observable
@MainActor
final class LocationWeatherManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private let weatherService = WeatherService.shared
var current: CurrentWeather?
var locationError: Error?
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
}
func requestWeather() {
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
}
nonisolated func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.last else { return }
Task { @MainActor in
do {
current = try await weatherService.weather(
for: location,
including: .current
)
} catch {
locationError = error
}
}
}
nonisolated func locationManager(
_ manager: CLLocationManager,
didFailWithError error: Error
) {
Task { @MainActor in
locationError = error
}
}
}References
Related skills
How it compares
Pick weatherkit for native iOS SwiftUI apps on Apple platforms; choose generic REST weather API skills when targeting cross-platform or non-Apple clients.
FAQ
Must attribution always show?
Yes. Apple requires combined mark and legal link wherever WeatherKit data appears or App Review may reject.
Should I call weather(for:) without including?
No when you only need current or alerts; selective queries reduce quota versus fetching all datasets.
How handle minute forecast unavailability?
Minute query returns optional nil in unsupported regions; use if let instead of force-unwrap.
Is Weatherkit safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.