Eigene WeatherCondition statt WeatherKits Typ: die Widget-Ebene soll nicht an
ein Framework gebunden sein, das eine kostenpflichtige Mitgliedschaft und eine
Entitlement voraussetzt. Fällt WeatherKit weg, wird nur die Abbildung getauscht.
Der Zwischenspeicher ist Teil der Funktion, kein Feinschliff: WeatherKit rechnet
nach Abrufen ab, und ein Widget, das bei jedem Öffnen des Panels neu lädt,
verbrennt das Kontingent für Daten, die sich stündlich ändern. 15 Minuten
Gültigkeit, und der Schlüssel führt den Ort mit — sonst zeigt das Widget nach
einem Ortswechsel weiter das alte Wetter, überzeugend, weil Zahlen dastehen.
Ein abgelaufener Stand bleibt als "letzter bekannter" erhalten. Bei einem
Netzfehler ist ein alter Messwert mit sichtbarem Zeitstempel ehrlicher und
nützlicher als ein leeres Feld; er wird abgeblendet und mit Warnzeichen gezeigt.
Apples Namensnennung steht im Datenmodell, nicht in einer Notiz: sie ist
Bedingung der Nutzung von WeatherKit und wird sonst beim Bauen der Oberfläche
übersehen. In der 2x1-Kachel als klickbarer Link, in der 1x1 im Tooltip.
Temperaturen ohne Nachkommastellen — die täuschen eine Genauigkeit vor, die
die Vorhersage nicht hat.
NICHT aktiviert: com.apple.developer.weatherkit. Die Entitlement erzwingt ein
echtes Provisioning-Profil, und die App-ID lässt sich nicht registrieren
("cannot be registered to your development team because it is not available").
Bis das im Portal geklärt ist, bleibt sie auskommentiert — die App baut und
läuft, das Widget meldet ehrlich "Wetter nicht abrufbar".
106 Tests grün.
126 lines
4.1 KiB
Swift
126 lines
4.1 KiB
Swift
import Foundation
|
|
|
|
/// Wetterlage, unabhängig von der Quelle.
|
|
///
|
|
/// Eine eigene Aufzählung statt WeatherKits Typ: die Widget-Ebene soll nicht
|
|
/// gegen ein Framework gebunden sein, das eine kostenpflichtige Mitgliedschaft
|
|
/// und eine Entitlement voraussetzt. Fällt WeatherKit einmal weg, wird nur die
|
|
/// Abbildung ausgetauscht.
|
|
public enum WeatherCondition: String, Equatable, Sendable, CaseIterable {
|
|
case clear
|
|
case mostlyClear
|
|
case partlyCloudy
|
|
case cloudy
|
|
case fog
|
|
case drizzle
|
|
case rain
|
|
case heavyRain
|
|
case sleet
|
|
case snow
|
|
case heavySnow
|
|
case hail
|
|
case thunderstorm
|
|
case windy
|
|
case hot
|
|
case frigid
|
|
case unknown
|
|
}
|
|
|
|
/// Der Ort, für den Wetter angezeigt wird.
|
|
public enum WeatherPlace: Equatable, Sendable, Codable {
|
|
/// Aktueller Standort über CoreLocation.
|
|
case current
|
|
/// Fester Ort, in den Widget-Einstellungen gewählt.
|
|
case fixed(name: String, latitude: Double, longitude: Double)
|
|
|
|
public var isCurrent: Bool { self == .current }
|
|
}
|
|
|
|
public struct HourlyPoint: Equatable, Sendable, Identifiable {
|
|
public let date: Date
|
|
public let temperature: Measurement<UnitTemperature>
|
|
public let condition: WeatherCondition
|
|
public let precipitationChance: Double
|
|
|
|
public var id: Date { date }
|
|
|
|
public init(date: Date, temperature: Measurement<UnitTemperature>,
|
|
condition: WeatherCondition, precipitationChance: Double) {
|
|
self.date = date
|
|
self.temperature = temperature
|
|
self.condition = condition
|
|
self.precipitationChance = precipitationChance
|
|
}
|
|
}
|
|
|
|
/// Was Apple bei Nutzung von WeatherKit sichtbar verlangt.
|
|
///
|
|
/// Das ist keine Kür: die Nennung samt Link auf die Rechtshinweise ist
|
|
/// Bedingung der Nutzung. Deshalb steht sie im Datenmodell und nicht in einer
|
|
/// Notiz, die man beim Bauen der Oberfläche übersieht.
|
|
public struct WeatherAttribution: Equatable, Sendable {
|
|
public let name: String
|
|
public let legalPageURL: URL
|
|
public let logoURL: URL?
|
|
|
|
public init(name: String, legalPageURL: URL, logoURL: URL?) {
|
|
self.name = name
|
|
self.legalPageURL = legalPageURL
|
|
self.logoURL = logoURL
|
|
}
|
|
}
|
|
|
|
public struct WeatherSnapshot: Equatable, Sendable {
|
|
public let placeName: String
|
|
public let temperature: Measurement<UnitTemperature>
|
|
public let apparentTemperature: Measurement<UnitTemperature>
|
|
public let condition: WeatherCondition
|
|
public let isDaylight: Bool
|
|
public let humidity: Double
|
|
public let windSpeed: Measurement<UnitSpeed>
|
|
public let high: Measurement<UnitTemperature>?
|
|
public let low: Measurement<UnitTemperature>?
|
|
public let hourly: [HourlyPoint]
|
|
public let asOf: Date
|
|
public let attribution: WeatherAttribution?
|
|
|
|
public init(placeName: String,
|
|
temperature: Measurement<UnitTemperature>,
|
|
apparentTemperature: Measurement<UnitTemperature>,
|
|
condition: WeatherCondition,
|
|
isDaylight: Bool,
|
|
humidity: Double,
|
|
windSpeed: Measurement<UnitSpeed>,
|
|
high: Measurement<UnitTemperature>?,
|
|
low: Measurement<UnitTemperature>?,
|
|
hourly: [HourlyPoint],
|
|
asOf: Date,
|
|
attribution: WeatherAttribution?) {
|
|
self.placeName = placeName
|
|
self.temperature = temperature
|
|
self.apparentTemperature = apparentTemperature
|
|
self.condition = condition
|
|
self.isDaylight = isDaylight
|
|
self.humidity = humidity
|
|
self.windSpeed = windSpeed
|
|
self.high = high
|
|
self.low = low
|
|
self.hourly = hourly
|
|
self.asOf = asOf
|
|
self.attribution = attribution
|
|
}
|
|
}
|
|
|
|
/// Wie beim Kalender: jeder Fehlerfall bekommt eine eigene Antwort in der
|
|
/// Oberfläche. „Keine Daten" ist keine.
|
|
public enum WeatherState: Equatable, Sendable {
|
|
case ready(WeatherSnapshot)
|
|
/// Noch nie geladen.
|
|
case idle
|
|
case loading
|
|
/// Ortungsberechtigung fehlt — betrifft nur `.current`.
|
|
case locationDenied
|
|
/// Kein Netz oder Dienst nicht erreichbar. Der letzte Stand bleibt sichtbar.
|
|
case unavailable(lastKnown: WeatherSnapshot?, reason: String)
|
|
}
|