Fix downloads never finishing due to duplicate AppEnvironment

SwiftUI evaluates a @State default-value autoclosure more than once,
keeping only the first instance but still running the side effects of
the discarded ones. Each AppEnvironment() built its own DownloadManager,
and each DownloadManager registered a background URLSession under the
same identifier. The download task started on the surviving instance's
session, but iOS delivered the completion delegate callback to the other
(leaked) instance, whose activeDownloads was empty - so the finished
file was dropped and the progress spinner spun forever at 0 KB.

- Make AppEnvironment a process-wide singleton (static let shared) and
  reference it from the App's @State, guaranteeing exactly one instance
  (and one background session, one DataManager/CloudKit stack).
- Make DownloadManager.setDownloadSettings idempotent: only create the
  background session when none exists, so a re-entrant call never
  invalidateAndCancels the live session and kills in-flight downloads.
  Cellular changes already route through refreshCellularAccessSetting().
This commit is contained in:
Arkadiusz Fal
2026-05-10 23:46:02 +02:00
parent 92d0b3215b
commit a734bb47f7
4 changed files with 25 additions and 6 deletions

View File

@@ -71,7 +71,7 @@ extension DownloadManager {
func loadDownloads() {
let decoder = JSONDecoder()
// ==== ACTIVE DOWNLOADS ====
if let activeData = UserDefaults.standard.data(forKey: "activeDownloads") {
LoggingService.shared.logDownload(

View File

@@ -146,10 +146,14 @@ final class DownloadManager: NSObject {
func setDownloadSettings(_ settings: DownloadSettings) {
let isInitialSetup = self.downloadSettings == nil
self.downloadSettings = settings
// Invalidate old session before creating new one with correct settings
urlSession?.invalidateAndCancel()
urlSession = nil
setupSession()
// Create the background session only once. The background URLSession's
// identifier is process-global, so tearing it down here would cancel any
// in-flight downloads. Cellular-access changes are handled separately via
// refreshCellularAccessSetting(), which migrates active downloads safely.
if urlSession == nil {
setupSession()
}
// Resume interrupted downloads only on initial setup
if isInitialSetup {