1.1: Offline-Cache + Outbox für Touren/Tagebuch, WeatherKit-Fix, Aufräumen
App-Review-Fix (Guideline 2.1 WeatherKit): - OneShotLocation: deterministisches async resolve() mit 10s-Timeout statt onChange-Lauschen; WetterView lädt bei fehlendem Standort einen Berlin-Fallback → kein ewiges Hängen bei "Hole Standort…", WeatherKit ist immer sichtbar. Offline-Lesen (SwiftData): - CachedRoute/CachedDiaryEntry/CachedImage + CachedAsyncImage: Touren, Tagebuch und Fotos werden cache-first geladen und sind offline verfügbar. - Cache wird bei Logout/401 geleert (RootView), kein Durchschimmern fremder User. Offline-Speichern (Outbox): - PendingRoute/PendingRoutePhoto: Tour inkl. unterwegs hinzugefügter Fotos wird offline lokal gesichert und automatisch hochgeladen (Touren-Tab + App-Start). - Touren-Liste zeigt offline gesicherte Touren mit "wird hochgeladen"-Badge. FinishWalkSheet: - Dismiss-Schutz: Speichern-Dialog lässt sich nicht mehr wegwischen — eine aufgezeichnete Tour geht nicht mehr durch Runterwischen verloren. Wetter: - Ortslabel (Reverse-Geocoding; Fallback "Berlin · Näherung"). - Saubere Offline-Meldung statt rohem networkError. Aufräumen: - Doppeltes "Gassi-Treffen" im Mehr-Tab entfernt. - Veraltete Phase-1/2-Texte neu getextet. - Tote DogsListView gelöscht (Hund-Wechsel läuft über den Heim-Picker).
This commit is contained in:
parent
9e51f3910e
commit
a2646a18ef
16 changed files with 769 additions and 199 deletions
|
|
@ -8,6 +8,16 @@ struct WetterView: View {
|
|||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var selectedDayIndex = 0
|
||||
@State private var placeName: String?
|
||||
@State private var isApproxLocation = false
|
||||
|
||||
/// Fallback-Standort (Berlin-Mitte): Wenn der Gerätestandort nicht
|
||||
/// ermittelbar ist (Timeout/verweigert — z. B. Apple-Review-iPad ohne
|
||||
/// Position), lädt WeatherKit trotzdem eine Vorhersage, statt ewig bei
|
||||
/// „Hole Standort…" zu hängen.
|
||||
private static let fallbackCoordinate = CLLocationCoordinate2D(
|
||||
latitude: 52.5200, longitude: 13.4050
|
||||
)
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
|
|
@ -33,11 +43,23 @@ struct WetterView: View {
|
|||
}
|
||||
.navigationTitle("Gassi-Wetter")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task { location.request() }
|
||||
.onChange(of: location.coordinate?.latitude) { _, _ in
|
||||
Task { await loadWeather() }
|
||||
.task {
|
||||
// Deterministisch: erst Standort auflösen (Fix/Fehler/Timeout),
|
||||
// dann laden — mit echtem Standort oder Berlin-Fallback. So hängt
|
||||
// der Screen nie bei „Hole Standort…" und WeatherKit lädt immer.
|
||||
let coord = await location.resolve()
|
||||
isApproxLocation = (coord == nil)
|
||||
let used = coord ?? Self.fallbackCoordinate
|
||||
await loadWeather(coord: used)
|
||||
placeName = isApproxLocation ? "Berlin" : await Self.reverseGeocode(used)
|
||||
}
|
||||
.refreshable {
|
||||
let coord = location.coordinate
|
||||
isApproxLocation = (coord == nil)
|
||||
let used = coord ?? Self.fallbackCoordinate
|
||||
await loadWeather(coord: used)
|
||||
placeName = isApproxLocation ? "Berlin" : await Self.reverseGeocode(used)
|
||||
}
|
||||
.refreshable { await loadWeather() }
|
||||
}
|
||||
|
||||
private func content(weather: Weather) -> some View {
|
||||
|
|
@ -46,6 +68,18 @@ struct WetterView: View {
|
|||
|
||||
return ScrollView {
|
||||
VStack(spacing: 14) {
|
||||
if let placeName {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: isApproxLocation ? "location.slash" : "location.fill")
|
||||
Text(placeName)
|
||||
if isApproxLocation {
|
||||
Text("· Näherung").foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
if !days.isEmpty {
|
||||
dayPicker(days: days)
|
||||
let day = days[safeIndex]
|
||||
|
|
@ -421,8 +455,7 @@ struct WetterView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func loadWeather() async {
|
||||
guard let coord = location.coordinate else { return }
|
||||
private func loadWeather(coord: CLLocationCoordinate2D) async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
|
|
@ -430,9 +463,25 @@ struct WetterView: View {
|
|||
do {
|
||||
weather = try await WeatherService.shared.weather(for: loc)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
errorMessage = Self.isOfflineError(error)
|
||||
? "Wetter ist offline nicht verfügbar. Die Vorhersage lädt automatisch, sobald du wieder Internet hast."
|
||||
: error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse-Geocoding → Ortsname (Stadt). Braucht Netz; offline → nil.
|
||||
private static func reverseGeocode(_ coord: CLLocationCoordinate2D) async -> String? {
|
||||
let loc = CLLocation(latitude: coord.latitude, longitude: coord.longitude)
|
||||
let placemarks = try? await CLGeocoder().reverseGeocodeLocation(loc)
|
||||
return placemarks?.first?.locality ?? placemarks?.first?.name
|
||||
}
|
||||
|
||||
private static func isOfflineError(_ error: Error) -> Bool {
|
||||
if error is URLError { return true }
|
||||
let d = error.localizedDescription.lowercased()
|
||||
return d.contains("internet") || d.contains("verbindung")
|
||||
|| d.contains("network") || d.contains("offline")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Day metrics derived from DayWeather
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue