CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

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 Condition Mapping
  • Caching Strategy
  • Location-Based Weather

WeatherKit SwiftUI Integration

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 fetchWeather(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.fetchWeather(for: location)
            }
            .refreshable {
                await manager.fetchWeather(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)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }
                .padding()
                .background(.yellow.opacity(0.1))
                .clipShape(.rect(cornerRadius: 8))
            }
        }
    }
}

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 historical weather data through daily and monthly statistics.

Daily Statistics

func fetchDailyStats(
    for location: CLLocation,
    dateRange: DateInterval
) async throws {
    let stats = try await WeatherService.shared.dailyStatistics(
        for: location,
        forDaysIn: dateRange,
        including: [.temperature, .precipitation]
    )

    for dayStat in stats {
        print("Date: \(dayStat.date)")
        if let temp = dayStat.statistics(for: .temperature) {
            print("  Avg temp: \(temp.mean?.formatted() ?? "N/A")")
            print("  Min temp: \(temp.minimum?.formatted() ?? "N/A")")
            print("  Max temp: \(temp.maximum?.formatted() ?? "N/A")")
        }
    }
}

Monthly Statistics

func fetchMonthlyStats(for location: CLLocation) async throws {
    let stats = try await WeatherService.shared.monthlyStatistics(
        for: location,
        including: [.temperature, .precipitation]
    )

    for monthStat in stats {
        print("Month: \(monthStat.month)")
    }
}

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 fetchDate: Date
    }

    private var cache: [String: CacheEntry] = [:]
    private let staleness: TimeInterval

    init(staleness: TimeInterval = 600) { // 10 minutes default
        self.staleness = staleness
    }

    func get(for key: String) -> CacheEntry? {
        guard let entry = cache[key],
              Date.now.timeIntervalSince(entry.fetchDate) < staleness 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,
            fetchDate: .now
        )
        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
        }
    }
}

skills

weatherkit

CHANGELOG.md

README.md

tile.json