379 lines
12 KiB
Markdown
379 lines
12 KiB
Markdown
# Offline-Konzept für Readeck iOS App
|
||
|
||
## Übersicht
|
||
|
||
Dieses Dokument beschreibt ein mehrstufiges Konzept zur Offline-Funktionalität der Readeck iOS App. Das Konzept ist modular aufgebaut und ermöglicht eine schrittweise Implementierung – von einer einfachen Caching-Strategie für ungelesene Artikel bis hin zu einer vollständig offline-fähigen App.
|
||
|
||
---
|
||
|
||
## Stufe 1: Smart Cache für Unread Items (Basis-Offline)
|
||
|
||
### Beschreibung
|
||
Die App lädt automatisch eine konfigurierbare Anzahl ungelesener Artikel herunter und hält diese offline verfügbar. Dies ist die Grundlage für eine bessere Offline-Erfahrung ohne großen Implementierungsaufwand.
|
||
|
||
### Features
|
||
- **Automatisches Caching**: Beim App-Start und bei Pull-to-Refresh werden die neuesten ungelesenen Artikel im Hintergrund heruntergeladen
|
||
- **Konfigurierbare Anzahl**: User kann in den Einstellungen festlegen, wie viele Artikel gecacht werden sollen (z.B. 10, 25, 50, 100)
|
||
- **Nur Artikel-Content**: Es wird nur der HTML-Content des Artikels (`/api/bookmarks/{id}/article`) gecached
|
||
- **Automatische Verwaltung**: Ältere gecachte Artikel werden automatisch entfernt, wenn neue hinzukommen (FIFO-Prinzip)
|
||
- **Offline-Indikator**: In der Bookmark-Liste wird angezeigt, welche Artikel offline verfügbar sind
|
||
|
||
### Technische Umsetzung
|
||
```swift
|
||
// Neue Settings
|
||
struct OfflineSettings {
|
||
var enabled: Bool = true
|
||
var maxUnreadArticles: Int = 25 // 10, 25, 50, 100
|
||
var onlyWiFi: Bool = true
|
||
}
|
||
|
||
// Neue Repository-Methode
|
||
protocol PBookmarksRepository {
|
||
func cacheBookmarkArticle(id: String, html: String) async throws
|
||
func getCachedArticle(id: String) -> String?
|
||
func hasCachedArticle(id: String) -> Bool
|
||
}
|
||
```
|
||
|
||
### Datenspeicherung
|
||
- **CoreData** für Artikel-HTML und Metadaten (Titel, URL, Datum, Reihenfolge)
|
||
- **FileManager** optional für große HTML-Dateien (falls CoreData zu groß wird)
|
||
|
||
### User Experience
|
||
- Bookmark-Liste zeigt Download-Icon für offline verfügbare Artikel
|
||
- Beim Öffnen eines gecachten Artikels: Sofortiges Laden ohne Netzwerk-Anfrage
|
||
- In Settings: "Offline-Modus" Sektion mit Slider für Anzahl der Artikel
|
||
- Cache-Größe wird angezeigt (z.B. "23 Artikel, 12.5 MB")
|
||
|
||
---
|
||
|
||
## Stufe 2: Offline-First mit Sync (Erweitert)
|
||
|
||
### Beschreibung
|
||
Die App funktioniert vollständig offline. Alle Aktionen werden lokal gespeichert und bei Netzwerkverbindung mit dem Server synchronisiert.
|
||
|
||
### Features
|
||
- **Vollständige Offline-Funktionalität**: Alle Lese-Operationen funktionieren offline
|
||
- **Lokale Schreib-Operationen**:
|
||
- Bookmarks erstellen, bearbeiten, löschen
|
||
- Labels hinzufügen/entfernen
|
||
- Artikel archivieren/favorisieren
|
||
- Lesefortschritt speichern
|
||
- Annotationen/Highlights erstellen
|
||
- **Intelligente Synchronisierung**:
|
||
- Automatische Sync bei Netzwerkverbindung
|
||
- Konfliktauflösung (Server gewinnt vs. Client gewinnt)
|
||
- Retry-Mechanismus bei fehlgeschlagenen Syncs
|
||
- **Sync-Status**: User sieht jederzeit, ob und was synchronisiert wird
|
||
- **Offline-Indikator**: Klarer Status in der UI (Online/Offline/Syncing)
|
||
|
||
### Technische Umsetzung
|
||
|
||
#### Sync-Manager
|
||
```swift
|
||
class OfflineSyncManager {
|
||
// Bereits vorhanden für Bookmark-Erstellung
|
||
// Erweitern für alle Operations
|
||
|
||
enum SyncOperation {
|
||
case createBookmark(CreateBookmarkRequest)
|
||
case updateBookmark(id: String, BookmarkUpdateRequest)
|
||
case deleteBookmark(id: String)
|
||
case addLabels(bookmarkId: String, labels: [String])
|
||
case removeLabels(bookmarkId: String, labels: [String])
|
||
case updateReadProgress(bookmarkId: String, progress: Int)
|
||
case createAnnotation(AnnotationRequest)
|
||
case deleteAnnotation(bookmarkId: String, annotationId: String)
|
||
}
|
||
|
||
func queueOperation(_ operation: SyncOperation) async
|
||
func syncAllPendingOperations() async throws
|
||
func getPendingOperationsCount() -> Int
|
||
}
|
||
```
|
||
|
||
#### Lokale Datenbank-Struktur
|
||
```swift
|
||
// CoreData Entities
|
||
entity OfflineBookmark {
|
||
id: String
|
||
title: String
|
||
url: String
|
||
content: String? // HTML
|
||
metadata: Data // JSON mit allen Bookmark-Daten
|
||
labels: [String]
|
||
isArchived: Bool
|
||
isMarked: Bool
|
||
readProgress: Int
|
||
annotations: [OfflineAnnotation]
|
||
lastModified: Date
|
||
syncStatus: String // "synced", "pending", "conflict"
|
||
}
|
||
|
||
entity PendingSyncOperation {
|
||
id: UUID
|
||
type: String // operation type
|
||
payload: Data // JSON der Operation
|
||
createdAt: Date
|
||
retryCount: Int
|
||
lastError: String?
|
||
}
|
||
```
|
||
|
||
### Sync-Strategien
|
||
|
||
#### Option A: Last-Write-Wins (Einfach)
|
||
- Server-Version überschreibt bei Konflikt immer lokale Version
|
||
- Einfach zu implementieren
|
||
- Potentieller Datenverlust bei Offline-Änderungen
|
||
|
||
#### Option B: Timestamp-basiert (Empfohlen)
|
||
- Neueste Änderung (basierend auf Timestamp) gewinnt
|
||
- Server sendet `updated` Timestamp mit jeder Response
|
||
- Client vergleicht mit lokalem Timestamp
|
||
|
||
#### Option C: Operational Transformation (Komplex)
|
||
- Granulare Merge-Strategien für verschiedene Felder
|
||
- Beispiel: Lokale Labels + Server Labels = Union
|
||
- Aufwändig, aber maximale Datenerhaltung
|
||
|
||
### User Experience
|
||
- **Offline-Banner**: "Du bist offline. Änderungen werden synchronisiert, sobald du online bist."
|
||
- **Sync-Status Indicator**:
|
||
- Grün: Alles synchronisiert
|
||
- Gelb: Synchronisierung läuft
|
||
- Rot: Sync-Fehler
|
||
- Grau: Offline
|
||
- **Pending Changes Badge**: Zeigt Anzahl nicht synchronisierter Änderungen
|
||
- **Manual Sync Button**: Manuelles Anstoßen der Synchronisierung
|
||
- **Sync-Konflikt-Dialog**: Bei Konflikten User entscheiden lassen (Lokal behalten / Server übernehmen / Beide behalten)
|
||
|
||
---
|
||
|
||
## Stufe 3: Vollständig Offline-Fähig (Maximum)
|
||
|
||
### Beschreibung
|
||
Die App kann komplett ohne Server-Verbindung genutzt werden, inklusive lokaler Volltext-Suche und erweiterter Offline-Features.
|
||
|
||
### Zusätzliche Features
|
||
- **Lokale Volltext-Suche**:
|
||
- SQLite FTS5 (Full-Text Search) für schnelle Suche
|
||
- Suche in Titeln, URLs, Content, Labels
|
||
- Highlighting von Suchbegriffen
|
||
- **Intelligente Offline-Strategie**:
|
||
- Predictive Caching basierend auf Leseverhalten
|
||
- Automatisches Herunterladen von "Ähnlichen Artikeln"
|
||
- Background Refresh für häufig gelesene Labels/Tags
|
||
- **Erweiterte Export-Funktionen**:
|
||
- Kompletten Offline-Cache als ZIP exportieren
|
||
- Import von Offline-Daten auf anderem Gerät
|
||
- Backup & Restore
|
||
- **Reader Mode Optimierungen**:
|
||
- Lokale Schriftarten für Offline-Nutzung
|
||
- CSS/JS lokal gespeichert
|
||
- Keine externen Dependencies
|
||
- **Offline-Statistiken**:
|
||
- Lesezeit offline vs. online
|
||
- Meistgelesene offline Artikel
|
||
- Speicherplatz-Statistiken
|
||
|
||
### Erweiterte Technische Umsetzung
|
||
|
||
#### FTS5 für Suche
|
||
```swift
|
||
// SQLite Schema
|
||
CREATE VIRTUAL TABLE bookmarks_fts USING fts5(
|
||
title,
|
||
url,
|
||
content,
|
||
labels,
|
||
content='offline_bookmarks',
|
||
content_rowid='id'
|
||
);
|
||
|
||
// Suche
|
||
func searchOfflineBookmarks(query: String) -> [Bookmark] {
|
||
let sql = """
|
||
SELECT * FROM offline_bookmarks
|
||
WHERE id IN (
|
||
SELECT rowid FROM bookmarks_fts
|
||
WHERE bookmarks_fts MATCH ?
|
||
)
|
||
ORDER BY rank
|
||
"""
|
||
// Execute and return results
|
||
}
|
||
```
|
||
|
||
#### Predictive Caching
|
||
```swift
|
||
class PredictiveCacheManager {
|
||
// Analysiere Leseverhalten
|
||
func analyzeReadingPatterns() -> ReadingProfile
|
||
|
||
// Lade ähnliche Artikel basierend auf:
|
||
// - Gleiche Labels/Tags
|
||
// - Gleiche Autoren
|
||
// - Gleiche Domains
|
||
func prefetchRelatedArticles(for bookmark: Bookmark) async
|
||
|
||
// Machine Learning (optional)
|
||
// CoreML Model für Content-Empfehlungen
|
||
func trainRecommendationModel()
|
||
}
|
||
```
|
||
|
||
#### Background Sync
|
||
```swift
|
||
// BackgroundTasks Framework
|
||
class BackgroundSyncScheduler {
|
||
func scheduleBackgroundSync() {
|
||
let request = BGAppRefreshTaskRequest(identifier: "de.readeck.sync")
|
||
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min
|
||
|
||
try? BGTaskScheduler.shared.submit(request)
|
||
}
|
||
|
||
func handleBackgroundSync(task: BGTask) async {
|
||
// Sync neue Artikel
|
||
// Update gecachte Artikel
|
||
// Cleanup alte Artikel
|
||
}
|
||
}
|
||
```
|
||
|
||
### Datenspeicherung
|
||
- **SQLite + FTS5**: Für Volltextsuche
|
||
- **CoreData**: Für strukturierte Daten und Relationships
|
||
- **FileManager**: Für HTML-Content, Bilder, Assets
|
||
- **NSUbiquitousKeyValueStore**: Optional für iCloud-Sync der Einstellungen
|
||
|
||
### User Experience
|
||
- **Offline-First Ansatz**: App fühlt sich immer schnell an, auch bei schlechter Verbindung
|
||
- **Smart Downloads**:
|
||
- "20 neue Artikel verfügbar. Jetzt herunterladen?"
|
||
- "Für dich empfohlen: 5 Artikel zum Offline-Lesen"
|
||
- **Storage Dashboard**:
|
||
- Visualisierung des genutzten Speichers
|
||
- Top 10 größte Artikel
|
||
- "Speicher optimieren" Funktion (Bilder komprimieren, alte Artikel löschen)
|
||
- **Offline-Modus Toggle**: Bewusster "Nur Offline"-Modus aktivierbar
|
||
- **Sync-Schedule**: "Täglich um 6:00 Uhr synchronisieren"
|
||
|
||
---
|
||
|
||
## Implementierungs-Roadmap
|
||
|
||
### Phase 1: Basis (Stufe 1) - ca. 2-3 Wochen
|
||
|
||
- [ ] Settings für Offline-Anzahl
|
||
- [ ] CoreData Schema für cached Articles
|
||
- [ ] Download-Logic in BookmarksRepository
|
||
- [ ] UI-Indikatoren für gecachte Artikel
|
||
- [ ] Cleanup-Logic (FIFO)
|
||
|
||
### Phase 2: Offline-First mit Sync (Stufe 2) - ca. 4-6 Wochen
|
||
|
||
- [ ] Erweiterte CoreData Entities
|
||
- [ ] OfflineSyncManager erweitern
|
||
- [ ] Konfliktauflösung implementieren
|
||
- [ ] Sync-Status UI
|
||
- [ ] Comprehensive Testing (Edge Cases)
|
||
|
||
### Phase 3: Advanced Features (Stufe 3) - ca. 4-6 Wochen
|
||
|
||
- [ ] SQLite FTS5 Integration
|
||
- [ ] Predictive Caching
|
||
- [ ] Background Tasks
|
||
- [ ] Export/Import
|
||
- [ ] Analytics & Optimizations
|
||
|
||
---
|
||
|
||
## Technische Überlegungen
|
||
|
||
### Performance
|
||
- **Lazy Loading**: Artikel-Content nur laden, wenn benötigt
|
||
- **Pagination**: Auch offline große Listen paginieren
|
||
- **Image Optimization**: Bilder komprimieren vor dem Speichern (WebP, HEIC)
|
||
- **Incremental Sync**: Nur Änderungen synchronisieren, nicht alles neu laden
|
||
|
||
### Speicherplatz
|
||
- **Quotas**: Maximale Größe für Offline-Cache (z.B. 500MB, 1GB, 2GB)
|
||
- **Cleanup-Strategien**:
|
||
- Älteste zuerst (FIFO)
|
||
- Größte zuerst
|
||
- Am wenigsten gelesen zuerst
|
||
- **Kompression**: HTML und JSON komprimieren (gzip)
|
||
|
||
### Sicherheit
|
||
- **Verschlüsselung**: Offline-Daten mit iOS Data Protection verschlüsseln
|
||
- **Sensitive Data**: Passwörter niemals lokal speichern (nur Token mit Keychain)
|
||
- **Cleanup bei Logout**: Alle Offline-Daten löschen
|
||
|
||
### Testing
|
||
- **Unit Tests**: Für Sync-Logic, Conflict Resolution
|
||
- **Integration Tests**: Offline→Online Szenarien
|
||
- **UI Tests**: Offline-Modi, Sync-Status
|
||
- **Edge Cases**:
|
||
- App-Kill während Sync
|
||
- Netzwerk-Loss während Download
|
||
- Speicher voll
|
||
- Server-Konflikte
|
||
|
||
---
|
||
|
||
## Bestehende Features die von Offline profitieren
|
||
|
||
### Bereits implementiert
|
||
- ✅ **Offline Bookmark Erstellung**: Bookmarks werden lokal gespeichert und bei Verbindung synchronisiert ([OfflineSyncManager.swift](readeck/Data/Repository/OfflineSyncManager.swift))
|
||
- ✅ **Server Reachability Check**: Check ob Server erreichbar ist vor Sync-Operationen
|
||
- ✅ **Sync Status UI**: isSyncing und syncStatus werden bereits getrackt
|
||
|
||
### Erweiterbar
|
||
- **Text-to-Speech**: Geht bereits offline, wenn Artikel gecacht ist
|
||
- **Annotations/Highlights**: Können offline erstellt und später synchronisiert werden
|
||
- **Lesefortschritt**: Kann lokal getrackt und bei Sync übertragen werden
|
||
- **Labels**: Offline hinzufügen/entfernen mit Sync
|
||
- **Read Progress**: Bereits vorhanden im BookmarkDetail Model, kann offline getrackt werden
|
||
|
||
---
|
||
|
||
## Metriken für Erfolg
|
||
|
||
### User-Metriken
|
||
- Durchschnittliche Offline-Nutzungszeit
|
||
- % der Nutzer, die Offline-Features aktivieren
|
||
- Anzahl der offline gelesenen Artikel pro User
|
||
- User-Feedback zur Offline-Erfahrung
|
||
|
||
### Technische Metriken
|
||
- Erfolgsrate der Synchronisierungen
|
||
- Durchschnittliche Sync-Dauer
|
||
- Anzahl der Sync-Konflikte
|
||
- Speicherplatz-Nutzung pro User
|
||
- Crash-Rate während Offline-Operationen
|
||
|
||
### Performance-Metriken
|
||
- App-Start-Zeit mit/ohne Offline-Cache
|
||
- Ladezeit für gecachte vs. nicht-gecachte Artikel
|
||
- Netzwerk-Traffic Reduktion durch Caching
|
||
- Battery Impact durch Background-Sync
|
||
|
||
---
|
||
|
||
## Nächste Schritte
|
||
|
||
1. **Entscheidung**: Welche Stufe soll zuerst implementiert werden?
|
||
2. **Prototyping**: Quick PoC für CoreData Schema und Cache-Logic
|
||
3. **UI/UX Design**: Mockups für Offline-Indikatoren und Settings
|
||
4. **Implementation**: Schrittweise nach Roadmap
|
||
5. **Testing**: Ausgiebiges Testen von Edge Cases
|
||
6. **Beta**: TestFlight mit fokussiertem Offline-Testing
|
||
7. **Launch**: Schrittweises Rollout mit Feature-Flags
|
||
|
||
---
|
||
|
||
*Dokument erstellt: 2025-11-01*
|
||
*Version: 1.0*
|