Major improvements to offline reading functionality:
**Hero Image Offline Support:**
- Add heroImageURL field to BookmarkEntity for persistent storage
- Implement ImageCache-based caching with custom keys (bookmark-{id}-hero)
- Update CachedAsyncImage to support offline loading via cache keys
- Hero images now work offline without URL dependency
**Offline Bookmark Loading:**
- Add proactive offline detection before API calls
- Implement automatic fallback to cached bookmarks when offline
- Fix network status initialization race condition
- Network monitor now checks status synchronously on init
**Core Data Enhancements:**
- Persist hero image URLs in BookmarkEntity.heroImageURL
- Reconstruct ImageResource from cached URLs on offline load
- Add extensive logging for debugging persistence issues
**UI Updates:**
- Update BookmarkDetailView2 to use cache keys for hero images
- Update BookmarkCardView (all 3 layouts) with cache key support
- Improve BookmarksView offline state handling with task-based loading
- Add 50ms delay for network status propagation
**Technical Details:**
- NetworkMonitorRepository: Fix initial status from hardcoded true to actual network check
- BookmarksViewModel: Inject AppSettings for offline detection
- OfflineCacheRepository: Add verification logging for save/load operations
- BookmarkEntityMapper: Sync heroImageURL on save, restore on load
This enables full offline reading with hero images visible in bookmark lists
and detail views, even after app restart.
111 lines
3.3 KiB
Swift
111 lines
3.3 KiB
Swift
//
|
|
// NetworkMonitorRepository.swift
|
|
// readeck
|
|
//
|
|
// Created by Claude on 18.11.25.
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import Combine
|
|
|
|
// MARK: - Protocol
|
|
|
|
protocol PNetworkMonitorRepository {
|
|
var isConnected: AnyPublisher<Bool, Never> { get }
|
|
func startMonitoring()
|
|
func stopMonitoring()
|
|
func reportConnectionFailure()
|
|
func reportConnectionSuccess()
|
|
}
|
|
|
|
// MARK: - Implementation
|
|
|
|
final class NetworkMonitorRepository: PNetworkMonitorRepository {
|
|
|
|
// MARK: - Properties
|
|
|
|
private let monitor = NWPathMonitor()
|
|
private let queue = DispatchQueue(label: "com.readeck.networkmonitor")
|
|
private let _isConnectedSubject: CurrentValueSubject<Bool, Never>
|
|
private var hasPathConnection = true
|
|
private var hasRealConnection = true
|
|
|
|
var isConnected: AnyPublisher<Bool, Never> {
|
|
_isConnectedSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
// Check current network status synchronously before starting monitor
|
|
let currentPath = monitor.currentPath
|
|
let hasInterfaces = currentPath.availableInterfaces.count > 0
|
|
let initialStatus = currentPath.status == .satisfied && hasInterfaces
|
|
|
|
_isConnectedSubject = CurrentValueSubject<Bool, Never>(initialStatus)
|
|
hasPathConnection = initialStatus
|
|
|
|
Logger.network.info("🌐 Initial network status: \(initialStatus ? "Connected" : "Offline")")
|
|
}
|
|
|
|
deinit {
|
|
monitor.cancel()
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
func startMonitoring() {
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
guard let self = self else { return }
|
|
|
|
// More sophisticated check: path must be satisfied AND have actual interfaces
|
|
let hasInterfaces = path.availableInterfaces.count > 0
|
|
let isConnected = path.status == .satisfied && hasInterfaces
|
|
|
|
self.hasPathConnection = isConnected
|
|
self.updateConnectionState()
|
|
|
|
// Log network changes with details
|
|
if path.status == .satisfied {
|
|
if hasInterfaces {
|
|
Logger.network.info("📡 Network path available (interfaces: \(path.availableInterfaces.count))")
|
|
} else {
|
|
Logger.network.warning("⚠️ Network path satisfied but no interfaces (VPN?)")
|
|
}
|
|
} else {
|
|
Logger.network.warning("📡 Network path unavailable")
|
|
}
|
|
}
|
|
|
|
monitor.start(queue: queue)
|
|
Logger.network.debug("Network monitoring started")
|
|
}
|
|
|
|
func stopMonitoring() {
|
|
monitor.cancel()
|
|
Logger.network.debug("Network monitoring stopped")
|
|
}
|
|
|
|
func reportConnectionFailure() {
|
|
hasRealConnection = false
|
|
updateConnectionState()
|
|
Logger.network.warning("⚠️ Real connection failure reported (VPN/unreachable server)")
|
|
}
|
|
|
|
func reportConnectionSuccess() {
|
|
hasRealConnection = true
|
|
updateConnectionState()
|
|
Logger.network.info("✅ Real connection success reported")
|
|
}
|
|
|
|
private func updateConnectionState() {
|
|
// Only connected if BOTH path is available AND real connection works
|
|
let isConnected = hasPathConnection && hasRealConnection
|
|
|
|
DispatchQueue.main.async {
|
|
self._isConnectedSubject.send(isConnected)
|
|
}
|
|
}
|
|
}
|