import Foundation /// Die letzte geglückte Antwort je Abruf, auf der Platte. /// /// Wozu: In der U-Bahn und im Ausland soll die App nicht mit einer /// Fehlermeldung dastehen, sondern den letzten Stand zeigen – mit Datum, damit /// niemand alte Zahlen für aktuelle hält. Geschrieben wird offline nicht. /// /// Bewusst schlicht: eine JSON-Datei je Schlüssel, kein Verfallsdatum, kein /// Aufräumen nach Grösse. Die Antworten sind klein (die grösste, die /// Buchungsliste, liegt im einstelligen Megabyte-Bereich), und ein Abmelden /// räumt ohnehin alles weg. enum Zwischenspeicher { /// Was aus dem Speicher kam, und wann es geholt wurde. struct Eintrag: Sendable where A: Sendable { let wert: A let geholtAm: Date } private static let ordner: URL? = { guard let basis = try? FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { return nil } let eigener = basis.appendingPathComponent("CertainAntworten", isDirectory: true) try? FileManager.default.createDirectory(at: eigener, withIntermediateDirectories: true) return eigener }() private static func datei(_ schluessel: String) -> URL? { // Der Schluessel ist ein Pfad mit Abfrage; daraus muss ein gueltiger // Dateiname werden. let sauber = schluessel.unicodeScalars.map { zeichen -> Character in CharacterSet.alphanumerics.contains(zeichen) ? Character(zeichen) : "_" } return ordner?.appendingPathComponent(String(sauber) + ".json") } static func lege(_ wert: A, unter schluessel: String) { guard let ziel = datei(schluessel), let daten = try? JSONEncoder().encode(wert) else { return } try? daten.write(to: ziel, options: .atomic) } static func hole(_ typ: A.Type, unter schluessel: String) -> Eintrag? { guard let quelle = datei(schluessel), let daten = try? Data(contentsOf: quelle), let wert = try? JSONDecoder().decode(A.self, from: daten) else { return nil } let wann = (try? quelle.resourceValues(forKeys: [.contentModificationDateKey]))? .contentModificationDate ?? Date() return Eintrag(wert: wert, geholtAm: wann) } static func leere() { guard let ordner else { return } try? FileManager.default.removeItem(at: ordner) } } /// Was eine Ansicht gerade anzeigen soll. /// /// Der vierte Fall ist der Grund für diesen Typ: Daten da, aber vom Band – /// die Ansicht muss das sagen können, ohne dass jede Ansicht ihre eigene /// Zustandsverwaltung erfindet. enum Ladezustand: Sendable { case laedt case da(A) /// Der Abruf schlug fehl, aber wir haben noch einen Stand von vorher. case ausSpeicher(A, geholtAm: Date, fehler: APIFehler) case fehler(APIFehler) var inhalt: A? { switch self { case .da(let wert): return wert case .ausSpeicher(let wert, _, _): return wert case .laedt, .fehler: return nil } } var laeuft: Bool { if case .laedt = self { return true } return false } }