ReadKeep/readeck/UI/Menu/OfflineBookmarksViewModel.swift
Ilyas Hallak ffb41347af refactor: Implement state machine architecture for offline sync
- Replace multiple boolean properties with single OfflineBookmarkSyncState enum
- Add Use Case pattern for OfflineSyncManager with dependency injection
- Simplify LocalBookmarksSyncView using state-driven UI with external bindings
- Convert OfflineBookmarksViewModel to use @Observable instead of ObservableObject
- Move credentials from Core Data to Keychain for better persistence
- Implement comprehensive database migration for App Group containers
- Add structured logging throughout sync operations and API calls

Architecture improvements follow MVVM principles with clean separation of concerns.
2025-08-18 22:58:42 +02:00

131 lines
4.2 KiB
Swift

import Foundation
import SwiftUI
import Combine
@Observable
class OfflineBookmarksViewModel {
var state: OfflineBookmarkSyncState = .idle
private let syncUseCase: POfflineBookmarkSyncUseCase
private var cancellables = Set<AnyCancellable>()
private var successTimer: Timer?
init(syncUseCase: POfflineBookmarkSyncUseCase = OfflineBookmarkSyncUseCase()) {
self.syncUseCase = syncUseCase
setupBindings()
updateState()
}
private func setupBindings() {
// Observe sync state changes
syncUseCase.isSyncing
.receive(on: DispatchQueue.main)
.sink { [weak self] isSyncing in
self?.handleSyncStateChange(isSyncing: isSyncing)
}
.store(in: &cancellables)
// Observe sync status changes
syncUseCase.syncStatus
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
self?.handleSyncStatusChange(status: status)
}
.store(in: &cancellables)
// Update count on app lifecycle events
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateState()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateState()
}
.store(in: &cancellables)
}
func updateState() {
let count = syncUseCase.getOfflineBookmarksCount()
switch state {
case .idle:
if count > 0 {
state = .pending(count: count)
}
case .pending:
if count > 0 {
state = .pending(count: count)
} else {
state = .idle
}
case .syncing:
// Keep syncing state, will be updated by handleSyncStateChange
break
case .success:
// Success state is temporary, handled by timer
break
case .error:
// Update count even in error state
if count > 0 {
state = .pending(count: count)
} else {
state = .idle
}
}
}
func syncOfflineBookmarks() async {
guard case .pending(let count) = state else { return }
state = .syncing(count: count, status: nil)
await syncUseCase.syncOfflineBookmarks()
}
private func handleSyncStateChange(isSyncing: Bool) {
if isSyncing {
// If we're not already in syncing state, transition to it
if case .pending(let count) = state {
state = .syncing(count: count, status: nil)
}
} else {
// Sync completed
Task { @MainActor in
// Small delay to ensure count is updated
try await Task.sleep(nanoseconds: 500_000_000)
let currentCount = syncUseCase.getOfflineBookmarksCount()
if case .syncing(let originalCount, _) = state {
if currentCount == 0 {
// Success - all bookmarks synced
state = .success(syncedCount: originalCount)
// Auto-hide success message after 2 seconds
successTimer?.invalidate()
successTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
self?.state = .idle
}
} else {
// Some bookmarks remain
state = .pending(count: currentCount)
}
}
}
}
}
private func handleSyncStatusChange(status: String?) {
if case .syncing(let count, _) = state {
state = .syncing(count: count, status: status)
}
}
deinit {
successTimer?.invalidate()
}
}