Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage, Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste Fachansicht die Kontenliste. Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen. Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
2.9 KiB
Swift
95 lines
2.9 KiB
Swift
import SwiftUI
|
||
|
||
/// Der Hinweis über einer Liste, deren Daten aus dem Zwischenspeicher kommen.
|
||
///
|
||
/// Wichtiger als er aussieht: Ohne ihn zeigt die App im Flugmodus alte Zahlen,
|
||
/// die aussehen wie frische. Mit Datum, weil „offline" allein nicht sagt, ob
|
||
/// der Stand von vor fünf Minuten oder von vor drei Wochen ist.
|
||
struct StandHinweis: View {
|
||
let geholtAm: Date
|
||
|
||
var body: some View {
|
||
Label {
|
||
Text(String(format: String(localized: "stand.hinweis"),
|
||
geholtAm.formatted(.relative(presentation: .named))))
|
||
} icon: {
|
||
Image(systemName: "wifi.slash")
|
||
}
|
||
.font(.footnote)
|
||
.foregroundStyle(.secondary)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.horizontal)
|
||
.padding(.vertical, 6)
|
||
.background(.quaternary.opacity(0.4))
|
||
}
|
||
}
|
||
|
||
/// Wenn gar nichts da ist: Fehler zeigen und, wo es Sinn ergibt, einen
|
||
/// Knopf zum Nochmalversuchen.
|
||
struct FehlerAnsicht: View {
|
||
let fehler: APIFehler
|
||
var nochmal: (() -> Void)?
|
||
|
||
var body: some View {
|
||
ContentUnavailableView {
|
||
Label(String(localized: "fehler.titel"), systemImage: symbol)
|
||
} description: {
|
||
Text(fehler.errorDescription ?? String(localized: "fehler.unbekannt"))
|
||
} actions: {
|
||
if let nochmal, fehler.lohntWiederholung {
|
||
Button(String(localized: "allgemein.nochmal"), action: nochmal)
|
||
.buttonStyle(.borderedProminent)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var symbol: String {
|
||
switch fehler {
|
||
case .keineVerbindung: return "wifi.slash"
|
||
case .nichtAngemeldet: return "person.crop.circle.badge.exclamationmark"
|
||
case .nichtGefunden: return "questionmark.folder"
|
||
default: return "exclamationmark.triangle"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Eine Liste, die noch nichts enthält – aber nicht wegen eines Fehlers.
|
||
struct LeerAnsicht: View {
|
||
let titel: String
|
||
let erklaerung: String
|
||
var symbol: String = "tray"
|
||
|
||
var body: some View {
|
||
ContentUnavailableView {
|
||
Label(titel, systemImage: symbol)
|
||
} description: {
|
||
Text(erklaerung)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Setzt die drei Zustände für eine Liste zusammen, damit das nicht jede
|
||
/// Ansicht neu baut.
|
||
struct Inhalt<A: Sendable, Darstellung: View>: View {
|
||
let zustand: Ladezustand<A>
|
||
var nochmal: (() -> Void)?
|
||
@ViewBuilder let darstellung: (A) -> Darstellung
|
||
|
||
var body: some View {
|
||
switch zustand {
|
||
case .laedt:
|
||
ProgressView()
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
case .da(let wert):
|
||
darstellung(wert)
|
||
case .ausSpeicher(let wert, let wann, _):
|
||
VStack(spacing: 0) {
|
||
StandHinweis(geholtAm: wann)
|
||
darstellung(wert)
|
||
}
|
||
case .fehler(let fehler):
|
||
FehlerAnsicht(fehler: fehler, nochmal: nochmal)
|
||
}
|
||
}
|
||
}
|