Bugfixes: - Add toggle for offline mode simulation (DEBUG only) - Fix VPN false-positives with interface count check - Add detailed error logging for download failures - Fix last sync timestamp display - Translate all strings to English Network Monitoring: - Add NetworkMonitorRepository with NWPathMonitor - Check path.status AND availableInterfaces for reliability - Add manual reportConnectionFailure/Success methods - Auto-load cached bookmarks when offline - Visual debug banner (green=online, red=offline) Architecture: - Clean architecture with Repository → UseCase → ViewModel - Network status in AppSettings for global access - Combine publishers for reactive updates
103 lines
2.9 KiB
Swift
103 lines
2.9 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>(true)
|
|
private var hasPathConnection = true
|
|
private var hasRealConnection = true
|
|
|
|
var isConnected: AnyPublisher<Bool, Never> {
|
|
_isConnectedSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
// Repository just manages the monitor, doesn't start it automatically
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|