import Foundation /// Hält den zuletzt geladenen Wetterstand je Ort. /// /// WeatherKit rechnet nach Abrufen ab. Ein Widget, das bei jedem Öffnen des /// Panels neu lädt, verbrennt das Kontingent für Daten, die sich stündlich /// ändern — der Zwischenspeicher ist deshalb Teil der Funktion und kein /// Feinschliff. public struct WeatherCache: Sendable { /// Wie lange ein Stand als aktuell gilt. public static let maxAge: TimeInterval = 15 * 60 private var entries: [String: WeatherSnapshot] = [:] public init() {} /// Der Stand, wenn er noch frisch genug ist. public func valid(for place: WeatherPlace, now: Date = Date()) -> WeatherSnapshot? { guard let entry = entries[Self.key(place)] else { return nil } return now.timeIntervalSince(entry.asOf) <= Self.maxAge ? entry : nil } /// Der letzte bekannte Stand, unabhängig vom Alter. /// /// Bei einem Netzfehler ist ein alter Messwert mit sichtbarem Zeitstempel /// deutlich besser als ein leeres Feld — man sieht, dass etwas da ist und /// wie alt es ist. public func lastKnown(for place: WeatherPlace) -> WeatherSnapshot? { entries[Self.key(place)] } public mutating func store(_ snapshot: WeatherSnapshot, for place: WeatherPlace) { entries[Self.key(place)] = snapshot } /// Der Schlüssel muss den Ort mitführen, sonst zeigt das Widget nach einem /// Ortswechsel weiter das alte Wetter — und zwar überzeugend, weil Zahlen /// dastehen. private static func key(_ place: WeatherPlace) -> String { switch place { case .current: "current" case .fixed(_, let latitude, let longitude): // Auf vier Nachkommastellen gerundet: das sind gut zehn Meter, // feiner unterscheidet sich das Wetter ohnehin nicht. String(format: "%.4f,%.4f", latitude, longitude) } } } public extension WeatherCondition { /// Das SF-Symbol zur Lage. /// /// Nur dort zwischen Tag und Nacht unterscheiden, wo es ein etabliertes /// Symbolpaar gibt. Eine erzwungene Unterscheidung erzeugt sonst Symbole, /// die niemand wiedererkennt. func symbolName(isDaylight: Bool) -> String { switch self { case .clear: isDaylight ? "sun.max.fill" : "moon.stars.fill" case .mostlyClear: isDaylight ? "sun.min.fill" : "moon.fill" case .partlyCloudy: isDaylight ? "cloud.sun.fill" : "cloud.moon.fill" case .cloudy: "cloud.fill" case .fog: "cloud.fog.fill" case .drizzle: "cloud.drizzle.fill" case .rain: "cloud.rain.fill" case .heavyRain: "cloud.heavyrain.fill" case .sleet: "cloud.sleet.fill" case .snow: "cloud.snow.fill" case .heavySnow: "snowflake" case .hail: "cloud.hail.fill" case .thunderstorm: "cloud.bolt.rain.fill" case .windy: "wind" case .hot: "thermometer.sun.fill" case .frigid: "thermometer.snowflake" case .unknown: "questionmark.circle" } } }