diff --git a/Yattee/Core/AppEnvironment.swift b/Yattee/Core/AppEnvironment.swift index dd95cb6b..8f00049e 100644 --- a/Yattee/Core/AppEnvironment.swift +++ b/Yattee/Core/AppEnvironment.swift @@ -56,6 +56,21 @@ final class AppEnvironment { let legacyMigrationService: LegacyDataMigrationService let sourcesSettings: SourcesSettings + // MARK: - Shared Instance + + /// The single, process-wide app environment. + /// + /// SwiftUI may evaluate a `@State` property's default-value autoclosure more + /// than once (it keeps only the first result but still runs the side effects + /// of the discarded instances). Constructing `AppEnvironment` more than once + /// would create multiple `DownloadManager`s — and therefore multiple + /// background `URLSession`s registered under the same identifier — causing + /// download-completion delegate callbacks to be delivered to an instance + /// whose `activeDownloads` is empty (the finished file is then dropped). + /// Referencing this `static let` from the App's `@State` guarantees exactly + /// one instance for the lifetime of the process. + static let shared = AppEnvironment() + // MARK: - Initialization init( diff --git a/Yattee/Services/Downloads/DownloadManager+Persistence.swift b/Yattee/Services/Downloads/DownloadManager+Persistence.swift index 70dfb7dd..764ff525 100644 --- a/Yattee/Services/Downloads/DownloadManager+Persistence.swift +++ b/Yattee/Services/Downloads/DownloadManager+Persistence.swift @@ -71,7 +71,7 @@ extension DownloadManager { func loadDownloads() { let decoder = JSONDecoder() - + // ==== ACTIVE DOWNLOADS ==== if let activeData = UserDefaults.standard.data(forKey: "activeDownloads") { LoggingService.shared.logDownload( diff --git a/Yattee/Services/Downloads/DownloadManager.swift b/Yattee/Services/Downloads/DownloadManager.swift index b49aed0d..fc406c7f 100644 --- a/Yattee/Services/Downloads/DownloadManager.swift +++ b/Yattee/Services/Downloads/DownloadManager.swift @@ -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 { diff --git a/Yattee/YatteeApp.swift b/Yattee/YatteeApp.swift index 213bf74b..f2131ef7 100644 --- a/Yattee/YatteeApp.swift +++ b/Yattee/YatteeApp.swift @@ -27,7 +27,7 @@ struct YatteeApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate #endif - @State private var appEnvironment = AppEnvironment() + @State private var appEnvironment = AppEnvironment.shared @State private var backgroundTasksRegistered = false @State private var showingClipboardAlert = false @State private var detectedClipboardURL: URL?