Widget-Optionen statt „Quellen", Reiterleiste von der Trennlinie herunter
Die Leiste saß mittig auf der Trennlinie unter dem Titelbalken. `titlebarAppearsTransparent` allein reichte nicht — macOS zieht darunter zusätzlich einen Trenner quer durchs Fenster. Der ist jetzt aus. Man setzt keinen Titel auf eine Trennlinie. „Quellen" war die Sicht des Programmierers: für den Nutzer ist die Datenquelle eine Einstellung **des Kalenderwidgets** unter mehreren. Der Bereich heißt jetzt „Widget-Optionen" und hat einen Abschnitt je Widget. Kalender: woher die Daten kommen, Monatsraster oder Terminliste — und endlich, welche Kalender überhaupt mitzählen. Gespeichert werden die **abgewählten**, nicht die gewählten: kommt ein neuer Kalender dazu, ist er damit automatisch dabei, statt dass man ihn suchen müsste, ohne zu wissen, dass es ihn gibt. Der Filter lässt die Abdeckung unberührt — welche Kalender man sehen will, sagt nichts darüber aus, für welchen Zeitraum Daten vorliegen. Wetter: dem Standort folgen oder einen festen Ort setzen, per Stadt oder Postleitzahl. WeatherKit will Koordinaten, der Nutzer kennt Ortsnamen — die Umsetzung passiert einmal beim Einstellen statt bei jedem Abruf. Der Ort überlebt jetzt auch den Neustart; vorher stand nach jedem Start wieder der Aufenthaltsort da. Die Ablage-Einstellungen sind vom Widget-Reiter hierher gewandert, wo die anderen Widget-Optionen stehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,22 @@ public final class WeatherKitSource {
|
||||
return fetched
|
||||
}
|
||||
|
||||
/// Koordinaten zu einem Ortsnamen.
|
||||
///
|
||||
/// Mit Zeitlimit wie alles andere hier: eine Ortssuche ohne Netz wartet
|
||||
/// sonst, bis jemand das Fenster schließt.
|
||||
public func coordinates(for query: String) async -> (name: String,
|
||||
coordinate: CLLocationCoordinate2D)? {
|
||||
let found = await withTimeout(seconds: 8) {
|
||||
try? await CLGeocoder().geocodeAddressString(query).first
|
||||
} ?? nil
|
||||
guard let placemark = found, let location = placemark.location else { return nil }
|
||||
let name = [placemark.locality, placemark.administrativeArea, placemark.country]
|
||||
.compactMap { $0 }
|
||||
.first ?? query
|
||||
return (name, location.coordinate)
|
||||
}
|
||||
|
||||
private func name(for location: CLLocation) async -> String {
|
||||
// Auch hier ein Zeitlimit: ohne Netz wartet die Umkehrsuche lange.
|
||||
// `CLGeocoder` ist nicht `Sendable`, also wird es im Aufruf angelegt
|
||||
|
||||
@@ -19,16 +19,30 @@ public final class WeatherModel {
|
||||
/// liefert nur den Wert zum Zeitpunkt des Lesens.
|
||||
public private(set) var locationAuthorization: LocationAuthorization = .undetermined
|
||||
public var place: WeatherPlace {
|
||||
didSet { guard place != oldValue else { return }; load(force: true) }
|
||||
didSet {
|
||||
guard place != oldValue else { return }
|
||||
if let data = try? JSONEncoder().encode(place) {
|
||||
defaults.set(data, forKey: Self.placeKey)
|
||||
}
|
||||
load(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
private static let placeKey = "weather.place"
|
||||
|
||||
private let source: WeatherKitSource
|
||||
private let defaults: UserDefaults
|
||||
private var cache = WeatherCache()
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
public init(source: WeatherKitSource = WeatherKitSource(), place: WeatherPlace = .current) {
|
||||
public init(source: WeatherKitSource = WeatherKitSource(),
|
||||
defaults: UserDefaults = .standard) {
|
||||
self.source = source
|
||||
self.place = place
|
||||
self.defaults = defaults
|
||||
// Der gewählte Ort überlebt den Neustart — sonst steht nach jedem
|
||||
// Start wieder das Wetter des Aufenthaltsorts da.
|
||||
self.place = defaults.data(forKey: Self.placeKey)
|
||||
.flatMap { try? JSONDecoder().decode(WeatherPlace.self, from: $0) } ?? .current
|
||||
// Sofort zeigen, was zuletzt bekannt war. Ein Wert von vor einer Stunde
|
||||
// mit Zeitstempel ist eine Auskunft; ein Rädchen ist keine.
|
||||
if let known = cache.lastKnown(for: place) {
|
||||
@@ -103,6 +117,23 @@ public final class WeatherModel {
|
||||
task?.cancel()
|
||||
task = nil
|
||||
}
|
||||
|
||||
/// Sucht die Koordinaten zu einem Ortsnamen und übernimmt ihn.
|
||||
///
|
||||
/// WeatherKit will Koordinaten, der Nutzer kennt Ortsnamen. Die Umsetzung
|
||||
/// dazwischen passiert einmal beim Einstellen, nicht bei jedem Abruf.
|
||||
///
|
||||
/// - Returns: der gefundene Ort, oder `nil` wenn es keinen gab.
|
||||
@discardableResult
|
||||
public func selectPlace(named query: String) async -> String? {
|
||||
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
guard let found = await source.coordinates(for: trimmed) else { return nil }
|
||||
place = .fixed(name: found.name,
|
||||
latitude: found.coordinate.latitude,
|
||||
longitude: found.coordinate.longitude)
|
||||
return found.name
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget
|
||||
|
||||
Reference in New Issue
Block a user