ReadKeep/documentation/Offline-Stufe1-Implementation-Tracking.md
Ilyas Hallak fdc6b3a6b6 Add offline reading UI and app integration (Phase 4 & 5)
Phase 4 - Settings UI:
- Add OfflineSettingsViewModel with reactive bindings
- Add OfflineSettingsView with toggle, slider, sync button
- Integrate into SettingsContainerView
- Extend factories with offline dependencies
- Add debug button to simulate offline mode (DEBUG only)

Phase 5 - App Integration:
- AppViewModel: Auto-sync on app start with 4h check
- BookmarksViewModel: Offline fallback loading cached articles
- BookmarksView: Offline banner when network unavailable
- BookmarkDetailViewModel: Cache-first article loading
- Fix concurrency issues with CurrentValueSubject

Features:
- Background sync on app start (non-blocking)
- Cached bookmarks shown when offline
- Instant article loading from cache
- Visual offline indicator banner
- Full offline reading experience

All features compile and build successfully.
2025-11-18 17:44:43 +01:00

20 KiB
Raw Blame History

Offline Stufe 1 - Implementierungs-Tracking

Branch: offline-sync Start: 2025-11-01 Geschätzte Dauer: 5-8 Tage


Phase 1: Foundation & Models (Tag 1)

1.1 Logging-Kategorie erweitern

Datei: readeck/Utils/Logger.swift

  • LogCategory.sync zur enum hinzufügen
  • Logger.sync zur Extension hinzufügen
  • Testen: Logging funktioniert

Code-Änderungen:

enum LogCategory: String, CaseIterable, Codable {
    // ... existing
    case sync = "Sync"
}

extension Logger {
    // ... existing
    static let sync = Logger(category: .sync)
}

1.2 Domain Model: OfflineSettings

Neue Datei: readeck/Domain/Model/OfflineSettings.swift

  • Struct OfflineSettings erstellen
  • Properties hinzufügen:
    • enabled: Bool = true
    • maxUnreadArticles: Double = 20
    • saveImages: Bool = false
    • lastSyncDate: Date?
  • Computed Property maxUnreadArticlesInt implementieren
  • Computed Property shouldSyncOnAppStart implementieren (4-Stunden-Check)
  • Codable Conformance
  • Testen: shouldSyncOnAppStart Logic

Checklist:

  • File erstellt
  • Alle Properties vorhanden
  • 4-Stunden-Check funktioniert
  • Kompiliert ohne Fehler

1.3 CoreData Entity: BookmarkEntity erweitert

Datei: readeck.xcdatamodeld

  • BookmarkEntity mit Cache-Feldern erweitern
  • Attributes definieren:
    • htmlContent (String)
    • cachedDate (Date, indexed)
    • lastAccessDate (Date)
    • cacheSize (Integer 64)
    • imageURLs (String, optional)
  • Lightweight Migration
  • Testen: App startet ohne Crash, keine Migration-Fehler

Checklist:

  • Cache-Felder hinzugefügt
  • Alle Attributes vorhanden
  • Indexes gesetzt
  • Migration funktioniert
  • App startet erfolgreich

Phase 2: Data Layer (Tag 1-2)

2.1 Settings Repository Protocol

Neue Datei: readeck/Domain/Protocols/PSettingsRepository.swift

  • Protocol PSettingsRepository erweitern
  • Methode loadOfflineSettings() definieren
  • Methode saveOfflineSettings(_ settings: OfflineSettings) definieren

Checklist:

  • Protocol erweitert
  • Methoden deklariert
  • Kompiliert ohne Fehler

2.2 Settings Repository Implementation

Datei: readeck/Data/Repository/SettingsRepository.swift

  • Class SettingsRepository erweitert
  • PSettingsRepository implementiert
  • loadOfflineSettings() implementiert:
    • UserDefaults laden
    • JSON dekodieren
    • Default-Settings zurückgeben bei Fehler
    • Logger.data für Erfolgsmeldung
  • saveOfflineSettings() implementiert:
    • JSON enkodieren
    • UserDefaults speichern
    • Logger.data für Erfolgsmeldung
  • Kompiliert ohne Fehler

Checklist:

  • File erweitert (Zeilen 274-296)
  • loadOfflineSettings() implementiert
  • saveOfflineSettings() implementiert
  • Logging integriert
  • Kompiliert erfolgreich

2.3 OfflineCacheRepository Protocol (ARCHITEKTUR-ÄNDERUNG)

Neue Datei: readeck/Domain/Protocols/POfflineCacheRepository.swift

HINWEIS: Anstatt PBookmarksRepository zu erweitern, wurde ein separates POfflineCacheRepository erstellt für Clean Architecture und Separation of Concerns.

  • Protocol POfflineCacheRepository erstellen
  • Neue Methoden zum Protocol hinzufügen:
    • cacheBookmarkWithMetadata(bookmark:html:saveImages:) async throws
    • getCachedArticle(id:) -> String?
    • hasCachedArticle(id:) -> Bool
    • getCachedBookmarks() async throws -> [Bookmark]
    • getCachedArticlesCount() -> Int
    • getCacheSize() -> String
    • clearCache() async throws
    • cleanupOldestCachedArticles(keepCount:) async throws

Checklist:

  • Protocol erstellt (Zeilen 1-24)
  • Alle Methoden deklariert
  • Async/throws korrekt gesetzt
  • Kompiliert ohne Fehler

2.4 OfflineCacheRepository Implementation (ARCHITEKTUR-ÄNDERUNG)

Neue Datei: readeck/Data/Repository/OfflineCacheRepository.swift

  • import Kingfisher hinzugefügt
  • cacheBookmarkWithMetadata() implementiert:
    • Prüfen ob bereits gecacht
    • Bookmark in CoreData speichern via BookmarkEntity
    • CoreData BookmarkEntity erweitern
    • Speichern in CoreData
    • Logger.sync.info
    • Bei saveImages: extractImageURLsFromHTML() aufrufen
    • Bei saveImages: prefetchImagesWithKingfisher() aufrufen
  • extractImageURLsFromHTML() implementiert:
    • Regex für <img src="..."> Tags
    • URLs extrahieren
    • Nur absolute URLs (http/https)
    • Logger.sync.debug
  • prefetchImagesWithKingfisher() implementiert:
    • URLs zu URL-Array konvertieren
    • ImagePrefetcher erstellt
    • Callback mit Logging
    • prefetcher.start()
    • Logger.sync.info
  • getCachedArticle() implementiert:
    • NSFetchRequest mit predicate
    • lastAccessDate updaten
    • htmlContent zurückgeben
  • hasCachedArticle() implementiert
  • getCachedBookmarks() implementiert:
    • Fetch alle BookmarkEntity mit htmlContent
    • Sort by cachedDate descending
    • toDomain() mapper nutzen
    • Array zurückgeben
  • getCachedArticlesCount() implementiert
  • getCacheSize() implementiert:
    • Alle sizes summieren
    • ByteCountFormatter nutzen
  • clearCache() implementiert:
    • Cache-Felder auf nil setzen
    • Logger.sync.info
  • cleanupOldestCachedArticles() implementiert:
    • Sort by cachedDate ascending
    • Älteste löschen wenn > keepCount
    • Logger.sync.info

Checklist:

  • File erstellt (272 Zeilen)
  • Kingfisher import
  • Alle Methoden implementiert
  • Logging überall integriert
  • Kompiliert ohne Fehler

Phase 3: Use Case & Business Logic (Tag 2)

3.1 OfflineCacheSyncUseCase Protocol

Neue Datei: readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift

  • Protocol POfflineCacheSyncUseCase erstellen
  • Published Properties definieren:
    • var isSyncing: AnyPublisher<Bool, Never>
    • var syncProgress: AnyPublisher<String?, Never>
  • Methoden deklarieren:
    • func syncOfflineArticles(settings:) async
    • func getCachedArticlesCount() -> Int
    • func getCacheSize() -> String

Checklist:

  • File erstellt
  • Protocol definiert (Zeilen 11-20)
  • Methoden deklariert
  • Kompiliert ohne Fehler

3.2 OfflineCacheSyncUseCase Implementation

Datei: readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift (im selben File)

  • Class OfflineCacheSyncUseCase erstellen
  • Dependencies:
    • offlineCacheRepository: POfflineCacheRepository
    • bookmarksRepository: PBookmarksRepository
    • settingsRepository: PSettingsRepository
  • CurrentValueSubject für State (statt @Published):
    • _isSyncingSubject = CurrentValueSubject<Bool, Never>(false)
    • _syncProgressSubject = CurrentValueSubject<String?, Never>(nil)
  • Publishers als computed properties
  • syncOfflineArticles() implementieren:
    • Guard enabled check
    • Logger.sync.info("Starting sync")
    • Set isSyncing = true
    • Fetch bookmarks (state: .unread, limit: settings.maxUnreadArticlesInt)
    • Logger.sync.info("Fetched X bookmarks")
    • Loop durch Bookmarks:
      • Skip wenn bereits gecacht (Logger.sync.debug)
      • syncProgress updaten: "Artikel X/Y..."
      • fetchBookmarkArticle() aufrufen
      • cacheBookmarkWithMetadata() aufrufen
      • successCount++
      • Bei saveImages: syncProgress "...+ Bilder"
      • Catch: errorCount++, Logger.sync.error
    • cleanupOldestCachedArticles() aufrufen
    • lastSyncDate updaten
    • Logger.sync.info(" Synced X, skipped Y, failed Z")
    • Set isSyncing = false
    • syncProgress = Status-Message
    • Sleep 3s, dann syncProgress = nil
  • getCachedArticlesCount() implementieren
  • getCacheSize() implementieren
  • Error-Handling:
    • Catch block für Haupt-Try
    • Logger.sync.error
    • syncProgress = Error-Message

Checklist:

  • Class erstellt (159 Zeilen)
  • Dependencies injiziert (3 repositories)
  • syncOfflineArticles() komplett mit @MainActor
  • Success/Skip/Error Tracking
  • Logging an allen wichtigen Stellen
  • Progress-Updates mit Emojis
  • Error-Handling
  • getCachedArticlesCount() fertig
  • getCacheSize() fertig
  • Kompiliert ohne Fehler

Phase 4: Settings UI (Tag 3)

4.1 OfflineSettingsViewModel

Neue Datei: readeck/UI/Settings/OfflineSettingsViewModel.swift

  • Class mit @Observable (ohne @MainActor auf Klassenebene)
  • Properties:
    • offlineSettings: OfflineSettings
    • isSyncing = false
    • syncProgress: String?
    • cachedArticlesCount = 0
    • cacheSize = "0 KB"
  • Dependencies:
    • settingsRepository: PSettingsRepository
    • offlineCacheSyncUseCase: POfflineCacheSyncUseCase
  • Init mit Factory
  • setupBindings() implementiert:
    • isSyncing Publisher binden
    • syncProgress Publisher binden
  • loadSettings() implementiert mit @MainActor
  • saveSettings() implementiert mit @MainActor
  • syncNow() implementiert mit @MainActor:
    • await offlineCacheSyncUseCase.syncOfflineArticles()
    • updateCacheStats()
  • updateCacheStats() implementiert mit @MainActor

Checklist:

  • File erstellt (89 Zeilen)
  • Properties definiert
  • Dependencies via Factory injiziert
  • setupBindings() mit Combine
  • Alle Methoden mit @MainActor markiert
  • Kompiliert ohne Fehler

4.2 OfflineSettingsView

Neue Datei: readeck/UI/Settings/OfflineSettingsView.swift

  • Struct OfflineSettingsView: View
  • @State viewModel
  • Body implementiert:
    • Section mit "Offline-Reading" header
    • Toggle: "Offline-Reading aktivieren" gebunden an enabled
    • If enabled:
      • VStack: Erklärungstext (caption, secondary)
      • VStack: Slider "Max. Artikel offline" (0-100, step 10)
      • HStack: Anzeige aktueller Wert
      • Toggle: "Bilder speichern" mit Erklärung
      • Button: "Jetzt synchronisieren" mit ProgressView
      • If syncProgress: Text anzeigen (caption)
      • If lastSyncDate: Text "Zuletzt: relative"
      • If cachedArticlesCount > 0: HStack mit Stats
  • task: loadSettings() bei Erscheinen
  • onChange Handler für alle Settings (auto-save)

Checklist:

  • File erstellt (145 Zeilen)
  • Form Structure mit Section
  • Toggle für enabled mit Erklärung
  • Slider für maxUnreadArticles mit Wert-Anzeige
  • Toggle für saveImages
  • Sync-Button mit Progress und Icon
  • Stats-Anzeige (Artikel + Größe)
  • Preview mit MockFactory
  • Kompiliert ohne Fehler

4.3 SettingsContainerView Integration

Datei: readeck/UI/Settings/SettingsContainerView.swift

  • OfflineSettingsView direkt eingebettet (kein NavigationLink)
  • Nach SyncSettingsView platziert
  • Konsistent mit anderen Settings-Sections

Checklist:

  • OfflineSettingsView() hinzugefügt (Zeile 28)
  • Korrekte Platzierung in der Liste
  • Kompiliert ohne Fehler

4.4 Factory erweitern

Dateien: readeck/UI/Factory/DefaultUseCaseFactory.swift + MockUseCaseFactory.swift

  • Protocol UseCaseFactory erweitert:
    • makeSettingsRepository() -> PSettingsRepository
    • makeOfflineCacheSyncUseCase() -> POfflineCacheSyncUseCase
  • DefaultUseCaseFactory implementiert:
    • offlineCacheRepository als lazy property
    • makeSettingsRepository() gibt settingsRepository zurück
    • makeOfflineCacheSyncUseCase() erstellt UseCase mit 3 Dependencies
  • MockUseCaseFactory implementiert:
    • MockSettingsRepository mit allen Methoden
    • MockOfflineCacheSyncUseCase mit Publishers

Checklist:

  • Protocol erweitert (2 neue Methoden)
  • DefaultUseCaseFactory: beide Methoden implementiert
  • MockUseCaseFactory: Mock-Klassen erstellt
  • ViewModel nutzt Factory korrekt
  • Kompiliert ohne Fehler
  • Test: ViewModel wird erstellt ohne Crash

4.5 MockUseCaseFactory erweitern (optional)

Datei: readeck/UI/Factory/MockUseCaseFactory.swift

  • Mock-Implementierungen für Tests hinzugefügt

Checklist:

  • MockSettingsRepository mit allen Protokoll-Methoden
  • MockOfflineCacheSyncUseCase mit Publishers
  • Kompiliert ohne Fehler

Phase 5: App Integration (Tag 3-4)

5.1 AppViewModel erweitern

Datei: readeck/UI/AppViewModel.swift

  • onAppStart() Methode um Sync erweitern:
    • Nach checkServerReachability()
    • syncOfflineArticlesIfNeeded() aufrufen (ohne await!)
  • syncOfflineArticlesIfNeeded() implementieren (private):
    • SettingsRepository instanziieren
    • Task.detached(priority: .background) starten
    • Settings laden
    • If shouldSyncOnAppStart:
      • Logger.sync.info("Auto-sync triggered")
      • syncUseCase holen via Factory
      • await syncOfflineArticles()
  • Testen: Auto-Sync bei App-Start (4h-Check)

Checklist:

  • onAppStart() erweitert
  • syncOfflineArticlesIfNeeded() implementiert
  • Task.detached mit .background
  • Kein await vor syncOfflineArticlesIfNeeded()
  • Logger.sync integriert
  • Test: App startet, Sync läuft im Hintergrund

5.2 BookmarksViewModel erweitern

Datei: readeck/UI/Bookmarks/BookmarksViewModel.swift

  • loadCachedBookmarks() implementieren (private):
    • bookmarksRepository.getCachedBookmarks() aufrufen
    • If nicht leer:
      • BookmarksPage erstellen mit gecachten Bookmarks
      • bookmarks Property setzen
      • hasMoreData = false
      • errorMessage beibehalten (für Banner)
      • Logger.viewModel.info
  • loadBookmarks() erweitern:
    • Im Network-Error catch block:
      • Nach isNetworkError = true
      • await loadCachedBookmarks() aufrufen
  • Testen: Bei Network-Error werden gecachte Bookmarks geladen

Checklist:

  • loadCachedBookmarks() implementiert
  • loadBookmarks() erweitert
  • Logger.viewModel integriert
  • Test: Offline-Modus zeigt gecachte Artikel

5.3 BookmarksView erweitern

Datei: readeck/UI/Bookmarks/BookmarksView.swift

  • offlineBanner View hinzufügen (private):
    • HStack mit wifi.slash Icon
    • Text "Offline-Modus Zeige gespeicherte Artikel"
    • Styling: caption, secondary, padding, background
  • body anpassen:
    • ZStack durch VStack(spacing: 0) ersetzen
    • If isNetworkError && bookmarks nicht leer:
      • offlineBanner anzeigen
    • Content darunter
    • FAB als Overlay über VStack
  • shouldShowCenteredState anpassen:
    • Kommentar: Nur bei leer UND error
    • return isEmpty && hasError
  • Testen: Offline-Banner erscheint bei Network-Error mit Daten

Checklist:

  • offlineBanner View erstellt
  • body mit VStack umgebaut
  • shouldShowCenteredState angepasst
  • Test: Banner wird angezeigt im Offline-Modus

5.4 BookmarkDetailViewModel erweitern

Datei: readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift

  • loadArticle() erweitern:
    • Vor Server-Request:
      • If let cachedHTML = bookmarksRepository.getCachedArticle(id:)
      • articleHTML = cachedHTML
      • isLoading = false
      • Logger.viewModel.info("Loaded from cache")
      • return
    • Nach Server-Request (im Task.detached):
      • Artikel optional cachen wenn saveImages enabled
  • Testen: Gecachte Artikel laden sofort

Checklist:

  • Cache-Check vor Server-Request
  • Logger.viewModel integriert
  • Optional: Background-Caching nach Load
  • Test: Gecachte Artikel laden instant

Phase 6: Testing & Polish (Tag 4-5)

6.1 Unit Tests

  • OfflineSettings Tests:
    • shouldSyncOnAppStart bei erstem Mal
    • shouldSyncOnAppStart nach 3h (false)
    • shouldSyncOnAppStart nach 5h (true)
    • shouldSyncOnAppStart bei disabled (false)
  • SettingsRepository Tests:
    • Save & Load roundtrip
    • Default values bei leerem UserDefaults
  • BookmarksRepository Cache Tests:
    • cacheBookmarkWithMetadata()
    • getCachedArticle()
    • hasCachedArticle()
    • cleanupOldestCachedArticles()
    • extractImageURLsFromHTML()

Checklist:

  • OfflineSettings Tests geschrieben
  • SettingsRepository Tests geschrieben
  • BookmarksRepository Tests geschrieben
  • Alle Tests grün

6.2 Integration Tests

  • App-Start Sync:
    • Erste Start: Sync läuft
    • Zweiter Start < 4h: Kein Sync
    • Nach 4h: Sync läuft
  • Manual Sync:
    • Button triggert Sync
    • Progress wird angezeigt
    • Success-Message erscheint
  • Offline-Modus:
    • Flugmodus aktivieren
    • Gecachte Bookmarks werden angezeigt
    • Offline-Banner erscheint
    • Artikel lassen sich öffnen
  • Cache Management:
    • 20 Artikel cachen
    • Stats zeigen 20 Artikel + Größe
    • Cleanup funktioniert bei Limit-Überschreitung

Checklist:

  • App-Start Sync getestet
  • Manual Sync getestet
  • Offline-Modus getestet
  • Cache Management getestet

6.3 Edge Cases

  • Netzwerk-Verlust während Sync:
    • Partial success wird geloggt
    • Status-Message korrekt
  • Speicher voll:
    • Fehlerbehandlung
    • User-Benachrichtigung
  • 100 Artikel Performance:
    • Sync dauert < 2 Minuten
    • App bleibt responsiv
  • CoreData Migration:
    • Alte App-Version → Neue Version
    • Keine Datenverluste
  • Kingfisher Cache:
    • Bilder werden geladen
    • Cache-Limit wird respektiert

Checklist:

  • Netzwerk-Verlust getestet
  • Speicher voll getestet
  • 100 Artikel Performance OK
  • Migration getestet
  • Kingfisher funktioniert

6.4 Bug-Fixing & Polish

  • Alle gefundenen Bugs gefixt
  • Code-Review durchgeführt
  • Logging überprüft (nicht zu viel, nicht zu wenig)
  • UI-Polish (Spacing, Colors, etc.)
  • Performance-Optimierungen falls nötig

Checklist:

  • Alle Bugs gefixt
  • Code reviewed
  • Logging optimiert
  • UI poliert
  • Performance OK

Final Checklist

Funktionalität

  • Offline-Reading Toggle funktioniert
  • Slider für Artikel-Anzahl funktioniert
  • Bilder-Toggle funktioniert
  • Auto-Sync bei App-Start (4h-Check)
  • Manual-Sync Button funktioniert
  • Offline-Modus zeigt gecachte Artikel
  • Offline-Banner wird angezeigt
  • Cache-Stats werden angezeigt
  • Last-Sync-Date wird angezeigt
  • Background-Sync mit niedriger Priority
  • Kingfisher cached Bilder
  • FIFO Cleanup funktioniert

Code-Qualität

  • Alle neuen Files erstellt
  • Alle Protokolle definiert
  • Alle Implementierungen vollständig
  • Logging überall integriert
  • Error-Handling implementiert
  • Keine Compiler-Warnings
  • Keine Force-Unwraps
  • Code dokumentiert (Kommentare wo nötig)

Tests

  • Unit Tests geschrieben
  • Integration Tests durchgeführt
  • Edge Cases getestet
  • Performance getestet
  • Alle Tests grün

Dokumentation

  • Implementierungsplan vollständig
  • Alle Checkboxen abgehakt
  • Gefundene Issues dokumentiert
  • Nächste Schritte (Stufe 2) überlegt

Commit & PR

  • Alle Änderungen commited
  • Commit-Messages aussagekräftig
  • Branch gepusht
  • PR erstellt gegen develop
  • PR-Beschreibung vollständig:
    • Was wurde implementiert
    • Wie testen
    • Screenshots (Settings-UI)
    • Known Issues (falls vorhanden)

Notes & Issues

Gefundene Probleme

(Hier während der Implementation eintragen)

Offene Fragen

(Hier während der Implementation eintragen)

Verbesserungsideen für Stufe 2

(Hier sammeln)


Erstellt: 2025-11-01 Letztes Update: 2025-11-01