20 KiB
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.synczur enum hinzufügenLogger.synczur 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 = truemaxUnreadArticles: Double = 20saveImages: Bool = falselastSyncDate: Date?
- Computed Property
maxUnreadArticlesIntimplementieren - Computed Property
shouldSyncOnAppStartimplementieren (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
PSettingsRepositoryerweitern - 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
SettingsRepositoryerweitert PSettingsRepositoryimplementiertloadOfflineSettings()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
POfflineCacheRepositoryerstellen - Neue Methoden zum Protocol hinzufügen:
cacheBookmarkWithMetadata(bookmark:html:saveImages:) async throwsgetCachedArticle(id:) -> String?hasCachedArticle(id:) -> BoolgetCachedBookmarks() async throws -> [Bookmark]getCachedArticlesCount() -> IntgetCacheSize() -> StringclearCache() async throwscleanupOldestCachedArticles(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 KingfisherhinzugefügtcacheBookmarkWithMetadata()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
- Regex für
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()implementiertgetCachedBookmarks()implementiert:- Fetch alle BookmarkEntity mit htmlContent
- Sort by cachedDate descending
- toDomain() mapper nutzen
- Array zurückgeben
getCachedArticlesCount()implementiertgetCacheSize()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
POfflineCacheSyncUseCaseerstellen - Published Properties definieren:
var isSyncing: AnyPublisher<Bool, Never>var syncProgress: AnyPublisher<String?, Never>
- Methoden deklarieren:
func syncOfflineArticles(settings:) asyncfunc getCachedArticlesCount() -> Intfunc 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
OfflineCacheSyncUseCaseerstellen - Dependencies:
offlineCacheRepository: POfflineCacheRepositorybookmarksRepository: PBookmarksRepositorysettingsRepository: 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()implementierengetCacheSize()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: OfflineSettingsisSyncing = falsesyncProgress: String?cachedArticlesCount = 0cacheSize = "0 KB"
- Dependencies:
settingsRepository: PSettingsRepositoryofflineCacheSyncUseCase: POfflineCacheSyncUseCase
- Init mit Factory
setupBindings()implementiert:- isSyncing Publisher binden
- syncProgress Publisher binden
loadSettings()implementiert mit @MainActorsaveSettings()implementiert mit @MainActorsyncNow()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
UseCaseFactoryerweitert:makeSettingsRepository() -> PSettingsRepositorymakeOfflineCacheSyncUseCase() -> POfflineCacheSyncUseCase
DefaultUseCaseFactoryimplementiert:offlineCacheRepositoryals lazy propertymakeSettingsRepository()gibt settingsRepository zurückmakeOfflineCacheSyncUseCase()erstellt UseCase mit 3 Dependencies
MockUseCaseFactoryimplementiert:MockSettingsRepositorymit allen MethodenMockOfflineCacheSyncUseCasemit 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
- Im Network-Error catch block:
- 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
offlineBannerView hinzufügen (private):- HStack mit wifi.slash Icon
- Text "Offline-Modus – Zeige gespeicherte Artikel"
- Styling: caption, secondary, padding, background
bodyanpassen:- ZStack durch VStack(spacing: 0) ersetzen
- If isNetworkError && bookmarks nicht leer:
- offlineBanner anzeigen
- Content darunter
- FAB als Overlay über VStack
shouldShowCenteredStateanpassen:- 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
- Vor Server-Request:
- 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