CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

weatherkit-patterns.mdskills/weatherkit/references/

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

skills

weatherkit

.mcp.json

README.md

tile.json