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:
rene 2026-06-02 19:37:30 +02:00
parent 9e51f3910e
commit a2646a18ef
16 changed files with 769 additions and 199 deletions

View file

@ -25,6 +25,7 @@ struct FinishWalkSheet: View {
@State private var saveState: SaveState = .idle
@State private var errorMessage: String?
@State private var savedOffline = false
private enum SaveState: Equatable {
case idle
@ -144,7 +145,15 @@ struct FinishWalkSheet: View {
.onChange(of: photoSelection) { _, newItems in
Task { await loadPhotos(from: newItems) }
}
.interactiveDismissDisabled(saveState != .idle)
// Immer blockieren: eine aufgezeichnete Tour darf nicht durch
// versehentliches Runterwischen verloren gehen nur Speichern"
// oder Verwerfen" beenden das Sheet.
.interactiveDismissDisabled(true)
.alert("Offline gespeichert", isPresented: $savedOffline) {
Button("OK") { onSaved(); dismiss() }
} message: {
Text("Keine Internetverbindung. Die Tour ist lokal gesichert und wird automatisch hochgeladen, sobald du wieder online bist.")
}
}
}
@ -283,6 +292,15 @@ struct FinishWalkSheet: View {
do {
route = try await APIClient.shared.post("/api/routes", body: body)
} catch {
// Transportfehler (offline) Tour inkl. Fotos lokal in die Outbox,
// wird automatisch hochgeladen, sobald wieder Netz da ist.
if error is URLError {
OfflineCache.savePendingRoute(body: body, photos: photoData, in: modelContext)
await syncHealthIfEnabled()
saveState = .idle
savedOffline = true
return
}
errorMessage = error.localizedDescription
saveState = .idle
return
@ -321,19 +339,21 @@ struct FinishWalkSheet: View {
saveState = .uploadingPhotos(done: photoData.count, total: photoData.count)
}
// Apple Health sync (only if user opted in)
if healthKitSyncEnabled, points.count >= 2 {
let endedAt = Date.now
let startedAt = endedAt.addingTimeInterval(-Double(durationSeconds))
await WalkHealthSync.shared.saveWalk(
points: points,
startedAt: startedAt,
endedAt: endedAt,
distanceMeters: distanceMeters
)
}
await syncHealthIfEnabled()
onSaved()
dismiss()
}
private func syncHealthIfEnabled() async {
guard healthKitSyncEnabled, points.count >= 2 else { return }
let endedAt = Date.now
let startedAt = endedAt.addingTimeInterval(-Double(durationSeconds))
await WalkHealthSync.shared.saveWalk(
points: points,
startedAt: startedAt,
endedAt: endedAt,
distanceMeters: distanceMeters
)
}
}