From 31d0e7b07986df1ca6197f4fe194c811516a88f4 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:33:51 +0530 Subject: [PATCH] feat: add offline network indicator for download screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add NetworkMonitor singleton (NWPathMonitor) that passively tracks network reachability on a background utility queue - Guard handleDownloadAction in DownloadStateManager to block downloads when offline and show a toast: 'No internet connection. Please connect and try again.' - Guard checkAllForUpdates to block the 'check for new data' spinner when offline with the same toast feedback - No existing logic removed — guards are purely additive early-returns - Reuses the existing ToastView/ToastModifier infrastructure Closes: offline silent failure on download screen --- Scribe.xcodeproj/project.pbxproj | 4 ++++ Scribe/Extensions/NetworkMonitor.swift | 20 +++++++++++++++++ .../DownloadStateManager.swift | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 Scribe/Extensions/NetworkMonitor.swift diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 6868221e..dc8f0bdb 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 073976D82F7C5EF3004F0674 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073976D72F7C5EF3004F0674 /* NetworkMonitor.swift */; }; 140158992A430DD000D14E52 /* ThirdPartyLicense.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158982A430DD000D14E52 /* ThirdPartyLicense.swift */; }; 1401589B2A45A07200D14E52 /* WikimediaAndScribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1401589A2A45A07200D14E52 /* WikimediaAndScribe.swift */; }; 140158A22A4EDB2200D14E52 /* TableViewTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */; }; @@ -1008,6 +1009,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 073976D72F7C5EF3004F0674 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; 140158982A430DD000D14E52 /* ThirdPartyLicense.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyLicense.swift; sourceTree = ""; }; 1401589A2A45A07200D14E52 /* WikimediaAndScribe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikimediaAndScribe.swift; sourceTree = ""; }; 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewTemplateViewController.swift; sourceTree = ""; }; @@ -2039,6 +2041,7 @@ children = ( EDEE62242B2DE65A00A0B9C1 /* UIEdgeInsetsExtensions.swift */, 84AF4D872C3575EA009AE0D2 /* UIDeviceExtensions.swift */, + 073976D72F7C5EF3004F0674 /* NetworkMonitor.swift */, ); path = Extensions; sourceTree = ""; @@ -2908,6 +2911,7 @@ D16DD3A529E78A1500FB9022 /* Utilities.swift in Sources */, EDB460212B03B3E400BEA967 /* BaseTableViewController.swift in Sources */, D1CDED752A859DDD00098546 /* DAInterfaceVariables.swift in Sources */, + 073976D82F7C5EF3004F0674 /* NetworkMonitor.swift in Sources */, 84AF4D882C3575EA009AE0D2 /* UIDeviceExtensions.swift in Sources */, 69B81EBC2BFB8C77008CAB85 /* TipCardView.swift in Sources */, 30453964293B9D18003AE55B /* InformationToolTipData.swift in Sources */, diff --git a/Scribe/Extensions/NetworkMonitor.swift b/Scribe/Extensions/NetworkMonitor.swift new file mode 100644 index 00000000..b8761e09 --- /dev/null +++ b/Scribe/Extensions/NetworkMonitor.swift @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import Network + +/// Lightweight singleton that tracks network reachability using NWPathMonitor. +final class NetworkMonitor { + static let shared = NetworkMonitor() + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "be.scri.networkMonitor", qos: .utility) + + private(set) var isConnected: Bool = true + + private init() { + monitor.pathUpdateHandler = { [weak self] path in + self?.isConnected = path.status == .satisfied + } + monitor.start(queue: queue) + } +} diff --git a/Scribe/InstallationTab/DownloadStateManager.swift b/Scribe/InstallationTab/DownloadStateManager.swift index ea57582d..e6f69806 100644 --- a/Scribe/InstallationTab/DownloadStateManager.swift +++ b/Scribe/InstallationTab/DownloadStateManager.swift @@ -41,6 +41,18 @@ class DownloadStateManager: ObservableObject { let currentState = downloadStates[key] ?? .ready let displayName = getKeyInDict(givenValue: key, dict: languagesAbbrDict) + // Block if offline. + guard NetworkMonitor.shared.isConnected else { + showToastMessage( + NSLocalizedString( + "i18n.app.download.error.no_internet", + value: "No internet connection. Please connect and try again.", + comment: "" + ) + ) + return + } + // Block if already downloading. if currentState == .downloading { return @@ -102,6 +114,16 @@ class DownloadStateManager: ObservableObject { /// Check all downloaded languages for updates. func checkAllForUpdates() { + guard NetworkMonitor.shared.isConnected else { + showToastMessage( + NSLocalizedString( + "i18n.app.download.error.no_internet", + value: "No internet connection. Please connect and try again.", + comment: "" + ) + ) + return + } for (language, state) in downloadStates where state == .updated { checkForUpdates(language: language) }