mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -71,7 +71,7 @@ extension DownloadManager {
|
||||
|
||||
func loadDownloads() {
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
|
||||
// ==== ACTIVE DOWNLOADS ====
|
||||
if let activeData = UserDefaults.standard.data(forKey: "activeDownloads") {
|
||||
LoggingService.shared.logDownload(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user