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).
51 lines
1.7 KiB
Swift
51 lines
1.7 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
import SwiftData
|
|
|
|
/// Wie `AsyncImage`, aber offline-fähig: zeigt zuerst ein lokal gecachtes Bild
|
|
/// (CachedImage in SwiftData); fehlt es, wird es remote geladen und dabei gleich
|
|
/// in den Offline-Cache geschrieben. `path` ist der relative Medienpfad
|
|
/// (z. B. "/media/routes/x.jpg"), die Basis-URL hängt OfflineCache an.
|
|
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
|
|
private let path: String?
|
|
private let content: (Image) -> Content
|
|
private let placeholder: () -> Placeholder
|
|
|
|
@Environment(\.modelContext) private var ctx
|
|
@State private var uiImage: UIImage?
|
|
|
|
init(
|
|
path: String?,
|
|
@ViewBuilder content: @escaping (Image) -> Content,
|
|
@ViewBuilder placeholder: @escaping () -> Placeholder
|
|
) {
|
|
self.path = path
|
|
self.content = content
|
|
self.placeholder = placeholder
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let uiImage {
|
|
content(Image(uiImage: uiImage))
|
|
} else {
|
|
placeholder()
|
|
}
|
|
}
|
|
.task(id: path) { await load() }
|
|
}
|
|
|
|
private func load() async {
|
|
uiImage = nil
|
|
guard let path, !path.isEmpty else { return }
|
|
if let data = OfflineCache.imageData(path: path, in: ctx), let img = UIImage(data: data) {
|
|
uiImage = img
|
|
return
|
|
}
|
|
guard let url = URL(string: OfflineCache.mediaBase + path) else { return }
|
|
if let (data, _) = try? await URLSession.shared.data(from: url), let img = UIImage(data: data) {
|
|
uiImage = img
|
|
OfflineCache.storeImage(path: path, data: data, in: ctx)
|
|
}
|
|
}
|
|
}
|