From cc6e4ea1c2e74b6a3e89975318ae9e8ee337f0cb Mon Sep 17 00:00:00 2001 From: William Boles Date: Fri, 18 Apr 2025 09:57:06 +0100 Subject: [PATCH 1/5] Switch MetaStore from being an actor to using GCD instead to control access to state --- .../BackgroundDownloadDelegator.swift | 67 ++++++------------- .../BackgroundDownloadMetaStore.swift | 42 +++++++----- .../BackgroundDownloadService.swift | 18 ++--- 3 files changed, 51 insertions(+), 76 deletions(-) diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift index 7b42998..260df39 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift @@ -12,6 +12,7 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { private let metaStore: BackgroundDownloadMetaStore private let taskStore: BackgroundDownloadTaskStore private let logger: Logger + private let processingGroup: DispatchGroup // MARK: - Init @@ -20,6 +21,7 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { self.metaStore = metaStore self.logger = logger self.taskStore = BackgroundDownloadTaskStore() + self.processingGroup = DispatchGroup() } // MARK: - URLSessionDownloadDelegate @@ -38,12 +40,13 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { try? FileManager.default.moveItem(at: location, to: tempLocation) - let processingTask = Task { + processingGroup.enter() + metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self, logger] metaData in defer { - cleanUpDownload(forURL: fromURL) + self?.metaStore.removeMetadata(key: fromURL.absoluteString) + self?.processingGroup.leave() } - - let metaData = await metaStore.retrieveMetadata(key: fromURL.absoluteString) + guard let metaData else { logger.error("Unable to find existing download item for: \(fromURL.absoluteString)") return @@ -66,12 +69,6 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { metaData.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error)) } } - - // TODO: Is this a race condition, where processing task could finish before this Task block is ran? - Task { - await taskStore.storeTask(processingTask, - key: fromURL.absoluteString) - } } func urlSession(_ session: URLSession, @@ -84,56 +81,32 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { logger.info("Download failed for: \(fromURL.absoluteString), error: \(error.localizedDescription)") - let processingTask = Task { + processingGroup.enter() + metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self] metaData in defer { - cleanUpDownload(forURL: fromURL) + self?.metaStore.removeMetadata(key: fromURL.absoluteString) + self?.processingGroup.leave() } - let metaData = await metaStore.retrieveMetadata(key: fromURL.absoluteString) metaData?.continuation?.resume(throwing: BackgroundDownloadError.clientError(error)) } - - // TODO: Is this a race condition, where processing task could finish before this Task block is ran? - Task { - await taskStore.storeTask(processingTask, - key: fromURL.absoluteString) - } } func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { logger.info("Did finish events for background session") - - Task { - await withTaskGroup(of: Void.self) { group in - for task in await taskStore.retrieveAll() { - group.addTask { - await task.value - } - } - - await group.waitForAll() - - logger.info("All tasks in group completed") - await MainActor.run { - guard let appDelegate = AppDelegate.shared else { - logger.error("App delegate is nil") - return - } - - appDelegate.backgroundDownloadsComplete() + processingGroup.notify(queue: .global()) { [logger] in + logger.info("Processing group has finished") + + DispatchQueue.main.async { + guard let appDelegate = AppDelegate.shared else { + logger.error("App Delegate is nil") + return } + + appDelegate.backgroundDownloadsComplete() } } } - - private func cleanUpDownload(forURL url: URL) { - Task { - let key = url.absoluteString - - await metaStore.removeMetadata(key: key) - await taskStore.removeTask(key: key) - } - } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift index 2888e2a..1e292ab 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift @@ -13,44 +13,56 @@ struct BackgroundDownloadMetaData { let continuation: CheckedContinuation? } -actor BackgroundDownloadMetaStore { +final class BackgroundDownloadMetaStore: @unchecked Sendable { private var inMemoryStore: [String: CheckedContinuation] private let persistentStore: UserDefaults + private let queue: DispatchQueue // MARK: - Init init() { self.inMemoryStore = [String: CheckedContinuation]() self.persistentStore = UserDefaults.standard + self.queue = DispatchQueue(label: "com.williamboles.background.download.service", + qos: .userInitiated, + attributes: .concurrent) } // MARK: - Store func storeMetadata(_ metaData: BackgroundDownloadMetaData, key: String) { - inMemoryStore[key] = metaData.continuation - persistentStore.set(metaData.toURL, forKey: key) + queue.async(flags: .barrier) { [weak self] in + self?.inMemoryStore[key] = metaData.continuation + self?.persistentStore.set(metaData.toURL, forKey: key) + } } // MARK: - Retrieve - func retrieveMetadata(key: String) -> BackgroundDownloadMetaData? { - guard let toURL = persistentStore.url(forKey: key) else { - return nil + func retrieveMetadata(key: String, + completionHandler: @escaping (@Sendable (BackgroundDownloadMetaData?) -> ())) { + return queue.async { [weak self] in + guard let toURL = self?.persistentStore.url(forKey: key) else { + completionHandler(nil) + return + } + + let continuation = self?.inMemoryStore[key] + + let metaData = BackgroundDownloadMetaData(toURL: toURL, + continuation: continuation) + + completionHandler(metaData) } - - let continuation = inMemoryStore[key] - - let metaData = BackgroundDownloadMetaData(toURL: toURL, - continuation: continuation) - - return metaData } // MARK: - Remove func removeMetadata(key: String) { - inMemoryStore.removeValue(forKey: key) - persistentStore.removeObject(forKey: key) + queue.async(flags: .barrier) { [weak self] in + self?.inMemoryStore.removeValue(forKey: key) + self?.persistentStore.removeObject(forKey: key) + } } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift index 6bbcf24..6dd6c94 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift @@ -51,24 +51,14 @@ actor BackgroundDownloadService { return try await withCheckedThrowingContinuation { continuation in logger.info("Scheduling download: \(fromURL.absoluteString)") - storeMetadata(from: fromURL, - to: toURL, - continuation: continuation) + let metaData = BackgroundDownloadMetaData(toURL: toURL, + continuation: continuation) + metaStore.storeMetadata(metaData, + key: fromURL.absoluteString) let downloadTask = session.downloadTask(with: fromURL) downloadTask.earliestBeginDate = Date().addingTimeInterval(10) // Remove this in production, the delay was added for demonstration purposes only downloadTask.resume() } } - - private func storeMetadata(from fromURL: URL, - to toURL: URL, - continuation: CheckedContinuation) { - Task { - let metaData = BackgroundDownloadMetaData(toURL: toURL, - continuation: continuation) - await metaStore.storeMetadata(metaData, - key: fromURL.absoluteString) - } - } } From fdc3aa5cee06fc307a68076247f5ddd2770890fe Mon Sep 17 00:00:00 2001 From: William Boles Date: Fri, 18 Apr 2025 15:26:52 +0100 Subject: [PATCH 2/5] Removed TaskStore --- .../project.pbxproj | 4 --- .../BackgroundDownloadDelegator.swift | 2 -- .../BackgroundDownloadTaskStore.swift | 31 ------------------- 3 files changed, 37 deletions(-) delete mode 100644 BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift diff --git a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj index f05f77c..56a356b 100644 --- a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 437CD5112D94C62800A909A6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CD50E2D94C62800A909A6 /* AppDelegate.swift */; }; 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */; }; 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */; }; - 4395E6632DB00E8300637803 /* BackgroundDownloadTaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */; }; 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */; }; 43D1CF5A2D80860E00AC1ED9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */; }; 43D1CF642D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF632D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift */; }; @@ -43,7 +42,6 @@ 437CD5132D94C66C00A909A6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = ""; }; 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadMetaStore.swift; sourceTree = ""; }; - 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadTaskStore.swift; sourceTree = ""; }; 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegator.swift; sourceTree = ""; }; 43D1CF4F2D80860C00AC1ED9 /* BackgroundTransferRevised-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BackgroundTransferRevised-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -110,7 +108,6 @@ 4395E65D2DB00E4100637803 /* BackgroundDownload */ = { isa = PBXGroup; children = ( - 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */, 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */, 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */, 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */, @@ -300,7 +297,6 @@ 437CD5102D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift in Sources */, 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */, 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */, - 4395E6632DB00E8300637803 /* BackgroundDownloadTaskStore.swift in Sources */, 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */, 43D1CF842D808A5000AC1ED9 /* ImageLoader.swift in Sources */, ); diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift index 260df39..fa4149d 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift @@ -10,7 +10,6 @@ import OSLog final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { private let metaStore: BackgroundDownloadMetaStore - private let taskStore: BackgroundDownloadTaskStore private let logger: Logger private let processingGroup: DispatchGroup @@ -20,7 +19,6 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { logger: Logger) { self.metaStore = metaStore self.logger = logger - self.taskStore = BackgroundDownloadTaskStore() self.processingGroup = DispatchGroup() } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift deleted file mode 100644 index 59400fe..0000000 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// BackgroundDownloadTaskStore.swift -// BackgroundTransferRevised-Example -// -// Created by William Boles on 16/04/2025. -// - -import Foundation - -actor BackgroundDownloadTaskStore { - private var tasks = [String: Task]() - - // MARK: - Add - - func storeTask(_ task: Task, - key: String) { - tasks[key] = task - } - - // MARK: - Retrieve - - func retrieveAll() -> [Task] { - Array(tasks.values) - } - - // MARK: - Remove - - func removeTask(key: String) { - tasks[key] = nil - } -} From 80052933eee258f1bc2fb24c48d42f19963f2240 Mon Sep 17 00:00:00 2001 From: William Boles Date: Sat, 19 Apr 2025 21:36:40 +0100 Subject: [PATCH 3/5] Updated naming --- .../project.pbxproj | 8 ++-- ...swift => BackgroundDownloadDelegate.swift} | 41 ++++++++++++------- .../BackgroundDownloadMetaStore.swift | 14 +++---- .../BackgroundDownloadService.swift | 15 ++----- 4 files changed, 41 insertions(+), 37 deletions(-) rename BackgroundTransferRevised-Example/Network/BackgroundDownload/{BackgroundDownloadDelegator.swift => BackgroundDownloadDelegate.swift} (74%) diff --git a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj index 56a356b..742251a 100644 --- a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 437CD5112D94C62800A909A6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CD50E2D94C62800A909A6 /* AppDelegate.swift */; }; 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */; }; 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */; }; - 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */; }; + 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegate.swift */; }; 43D1CF5A2D80860E00AC1ED9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */; }; 43D1CF642D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF632D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift */; }; 43D1CF832D808A5000AC1ED9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF7E2D808A5000AC1ED9 /* Assets.xcassets */; }; @@ -42,7 +42,7 @@ 437CD5132D94C66C00A909A6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = ""; }; 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadMetaStore.swift; sourceTree = ""; }; - 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegator.swift; sourceTree = ""; }; + 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegate.swift; sourceTree = ""; }; 43D1CF4F2D80860C00AC1ED9 /* BackgroundTransferRevised-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BackgroundTransferRevised-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 43D1CF5F2D80860E00AC1ED9 /* BackgroundTransferRevised-ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BackgroundTransferRevised-ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -110,7 +110,7 @@ children = ( 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */, 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */, - 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */, + 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegate.swift */, ); path = BackgroundDownload; sourceTree = ""; @@ -297,7 +297,7 @@ 437CD5102D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift in Sources */, 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */, 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */, - 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */, + 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegate.swift in Sources */, 43D1CF842D808A5000AC1ED9 /* ImageLoader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift similarity index 74% rename from BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift rename to BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift index fa4149d..15b25bf 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift @@ -8,17 +8,23 @@ import Foundation import OSLog -final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { +enum BackgroundDownloadError: Error { + case fileSystemError(_ underlyingError: Error) + case clientError(_ underlyingError: Error) + case serverError(_ underlyingResponse: URLResponse?) +} + +final class BackgroundDownloadDelegate: NSObject, URLSessionDownloadDelegate { private let metaStore: BackgroundDownloadMetaStore private let logger: Logger private let processingGroup: DispatchGroup // MARK: - Init - init(metaStore: BackgroundDownloadMetaStore, - logger: Logger) { + init(metaStore: BackgroundDownloadMetaStore) { self.metaStore = metaStore - self.logger = logger + self.logger = Logger(subsystem: "com.williamboles", + category: "background.download.delegate") self.processingGroup = DispatchGroup() } @@ -39,20 +45,21 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { to: tempLocation) processingGroup.enter() - metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self, logger] metaData in + metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self, logger] metadata in defer { self?.metaStore.removeMetadata(key: fromURL.absoluteString) self?.processingGroup.leave() } - guard let metaData else { + guard let metadata else { logger.error("Unable to find existing download item for: \(fromURL.absoluteString)") return } - guard let response = downloadTask.response as? HTTPURLResponse, response.statusCode == 200 else { + guard let response = downloadTask.response as? HTTPURLResponse, + response.statusCode == 200 else { logger.error("Unexpected response for: \(fromURL.absoluteString)") - metaData.continuation?.resume(throwing: BackgroundDownloadError.serverError(downloadTask.response)) + metadata.continuation?.resume(throwing: BackgroundDownloadError.serverError(downloadTask.response)) return } @@ -60,11 +67,11 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { do { try FileManager.default.moveItem(at: tempLocation, - to: metaData.toURL) - metaData.continuation?.resume(returning: metaData.toURL) + to: metadata.toURL) + metadata.continuation?.resume(returning: metadata.toURL) } catch { logger.error("File system error while moving file: \(error.localizedDescription)") - metaData.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error)) + metadata.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error)) } } } @@ -72,21 +79,25 @@ final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let error = error, - let fromURL = task.originalRequest?.url else { + guard let error = error else { + return + } + + guard let fromURL = task.originalRequest?.url else { + logger.error("Unexpected nil URL for task.") return } logger.info("Download failed for: \(fromURL.absoluteString), error: \(error.localizedDescription)") processingGroup.enter() - metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self] metaData in + metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self] metadata in defer { self?.metaStore.removeMetadata(key: fromURL.absoluteString) self?.processingGroup.leave() } - metaData?.continuation?.resume(throwing: BackgroundDownloadError.clientError(error)) + metadata?.continuation?.resume(throwing: BackgroundDownloadError.clientError(error)) } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift index 1e292ab..fcb0330 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift @@ -8,7 +8,7 @@ import Foundation -struct BackgroundDownloadMetaData { +struct BackgroundDownloadMetadata { let toURL: URL let continuation: CheckedContinuation? } @@ -30,18 +30,18 @@ final class BackgroundDownloadMetaStore: @unchecked Sendable { // MARK: - Store - func storeMetadata(_ metaData: BackgroundDownloadMetaData, + func storeMetadata(_ metadata: BackgroundDownloadMetadata, key: String) { queue.async(flags: .barrier) { [weak self] in - self?.inMemoryStore[key] = metaData.continuation - self?.persistentStore.set(metaData.toURL, forKey: key) + self?.inMemoryStore[key] = metadata.continuation + self?.persistentStore.set(metadata.toURL, forKey: key) } } // MARK: - Retrieve func retrieveMetadata(key: String, - completionHandler: @escaping (@Sendable (BackgroundDownloadMetaData?) -> ())) { + completionHandler: @escaping (@Sendable (BackgroundDownloadMetadata?) -> ())) { return queue.async { [weak self] in guard let toURL = self?.persistentStore.url(forKey: key) else { completionHandler(nil) @@ -50,10 +50,10 @@ final class BackgroundDownloadMetaStore: @unchecked Sendable { let continuation = self?.inMemoryStore[key] - let metaData = BackgroundDownloadMetaData(toURL: toURL, + let metadata = BackgroundDownloadMetadata(toURL: toURL, continuation: continuation) - completionHandler(metaData) + completionHandler(metadata) } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift index 6dd6c94..fd07982 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift @@ -11,12 +11,6 @@ import OSLog import UIKit import SwiftUI -enum BackgroundDownloadError: Error { - case fileSystemError(_ underlyingError: Error) - case clientError(_ underlyingError: Error) - case serverError(_ underlyingResponse: URLResponse?) -} - actor BackgroundDownloadService { private let session: URLSession private let metaStore: BackgroundDownloadMetaStore @@ -33,14 +27,13 @@ actor BackgroundDownloadService { self.logger = Logger(subsystem: "com.williamboles", category: "background.download") - let delegator = BackgroundDownloadDelegator(metaStore: metaStore, - logger: logger) + let delegate = BackgroundDownloadDelegate(metaStore: metaStore) let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session") configuration.isDiscretionary = false configuration.sessionSendsLaunchEvents = true self.session = URLSession(configuration: configuration, - delegate: delegator, + delegate: delegate, delegateQueue: nil) } @@ -51,9 +44,9 @@ actor BackgroundDownloadService { return try await withCheckedThrowingContinuation { continuation in logger.info("Scheduling download: \(fromURL.absoluteString)") - let metaData = BackgroundDownloadMetaData(toURL: toURL, + let metadata = BackgroundDownloadMetadata(toURL: toURL, continuation: continuation) - metaStore.storeMetadata(metaData, + metaStore.storeMetadata(metadata, key: fromURL.absoluteString) let downloadTask = session.downloadTask(with: fromURL) From 75705d0304f60e7ad9ded836d89ae716a14905c9 Mon Sep 17 00:00:00 2001 From: William Boles Date: Sat, 19 Apr 2025 22:34:54 +0100 Subject: [PATCH 4/5] Pass closure to service --- .../Application/AppDelegate.swift | 19 +++---------- ...BackgroundTransferRevised_ExampleApp.swift | 19 ++++--------- .../BackgroundDownloadDelegate.swift | 28 ++++++++----------- .../BackgroundDownloadService.swift | 21 +++++++++++++- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/BackgroundTransferRevised-Example/Application/AppDelegate.swift b/BackgroundTransferRevised-Example/Application/AppDelegate.swift index 59958e6..8be131f 100644 --- a/BackgroundTransferRevised-Example/Application/AppDelegate.swift +++ b/BackgroundTransferRevised-Example/Application/AppDelegate.swift @@ -8,25 +8,14 @@ import UIKit import OSLog -class AppDelegate: NSObject, UIApplicationDelegate { - static var shared: AppDelegate? - - private var backgroundCompletionHandler: (() -> Void)? - private let logger = Logger(subsystem: "com.williamboles", - category: "appDelegate") - +class AppDelegate: NSObject, UIApplicationDelegate { // MARK: - Background func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { - self.backgroundCompletionHandler = completionHandler - } - - func backgroundDownloadsComplete() { - logger.info("Triggering background session completion handler") - - backgroundCompletionHandler?() - backgroundCompletionHandler = nil + Task { + await BackgroundDownloadService.shared.saveBackgroundCompletionHandler(completionHandler) + } } } diff --git a/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift index 61b5077..47a4bdb 100644 --- a/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift +++ b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift @@ -16,13 +16,6 @@ struct BackgroundTransferRevised_ExampleApp: App { private let logger = Logger(subsystem: "com.williamboles", category: "app") - - // MARK: - Init - - init() { - AppDelegate.shared = appDelegate - } - // MARK: - Scene var body: some Scene { @@ -37,12 +30,12 @@ struct BackgroundTransferRevised_ExampleApp: App { logger.info("Files will be downloaded to: \(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteString)") -// //Exit app to test restoring app from a terminated state. Comment out to test restoring app from a suspended state. -// Task { -// logger.info("Simulating app termination by exit(0)") -// -// exit(0) -// } + //Exit app to test restoring app from a terminated state. Comment out to test restoring app from a suspended state. + Task { + logger.info("Simulating app termination by exit(0)") + + exit(0) + } } } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift index 15b25bf..1aaeb48 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegate.swift @@ -18,14 +18,17 @@ final class BackgroundDownloadDelegate: NSObject, URLSessionDownloadDelegate { private let metaStore: BackgroundDownloadMetaStore private let logger: Logger private let processingGroup: DispatchGroup + private let urlSessionDidFinishEventsCompletionHandler: (@Sendable () -> Void) // MARK: - Init - init(metaStore: BackgroundDownloadMetaStore) { + init(metaStore: BackgroundDownloadMetaStore, + urlSessionDidFinishEventsCompletionHandler: @escaping (@Sendable () -> Void)) { self.metaStore = metaStore self.logger = Logger(subsystem: "com.williamboles", category: "background.download.delegate") self.processingGroup = DispatchGroup() + self.urlSessionDidFinishEventsCompletionHandler = urlSessionDidFinishEventsCompletionHandler } // MARK: - URLSessionDownloadDelegate @@ -45,32 +48,32 @@ final class BackgroundDownloadDelegate: NSObject, URLSessionDownloadDelegate { to: tempLocation) processingGroup.enter() - metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self, logger] metadata in + metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self] metadata in defer { self?.metaStore.removeMetadata(key: fromURL.absoluteString) self?.processingGroup.leave() } guard let metadata else { - logger.error("Unable to find existing download item for: \(fromURL.absoluteString)") + self?.logger.error("Unable to find existing download item for: \(fromURL.absoluteString)") return } guard let response = downloadTask.response as? HTTPURLResponse, response.statusCode == 200 else { - logger.error("Unexpected response for: \(fromURL.absoluteString)") + self?.logger.error("Unexpected response for: \(fromURL.absoluteString)") metadata.continuation?.resume(throwing: BackgroundDownloadError.serverError(downloadTask.response)) return } - logger.info("Download successful for: \(fromURL.absoluteString)") + self?.logger.info("Download successful for: \(fromURL.absoluteString)") do { try FileManager.default.moveItem(at: tempLocation, to: metadata.toURL) metadata.continuation?.resume(returning: metadata.toURL) } catch { - logger.error("File system error while moving file: \(error.localizedDescription)") + self?.logger.error("File system error while moving file: \(error.localizedDescription)") metadata.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error)) } } @@ -104,17 +107,10 @@ final class BackgroundDownloadDelegate: NSObject, URLSessionDownloadDelegate { func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { logger.info("Did finish events for background session") - processingGroup.notify(queue: .global()) { [logger] in - logger.info("Processing group has finished") + processingGroup.notify(queue: .global()) { [weak self] in + self?.logger.info("Processing group has finished") - DispatchQueue.main.async { - guard let appDelegate = AppDelegate.shared else { - logger.error("App Delegate is nil") - return - } - - appDelegate.backgroundDownloadsComplete() - } + self?.urlSessionDidFinishEventsCompletionHandler() } } } diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift index fd07982..01e828b 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift @@ -16,6 +16,8 @@ actor BackgroundDownloadService { private let metaStore: BackgroundDownloadMetaStore private let logger: Logger + private var backgroundCompletionHandler: (() -> Void)? + // MARK: - Singleton static let shared = BackgroundDownloadService() @@ -27,7 +29,11 @@ actor BackgroundDownloadService { self.logger = Logger(subsystem: "com.williamboles", category: "background.download") - let delegate = BackgroundDownloadDelegate(metaStore: metaStore) + let delegate = BackgroundDownloadDelegate(metaStore: metaStore) { + Task { + await BackgroundDownloadService.shared.backgroundDownloadsComplete() + } + } let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session") configuration.isDiscretionary = false @@ -54,4 +60,17 @@ actor BackgroundDownloadService { downloadTask.resume() } } + + // MARK: - CompletionHandler + + func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: @escaping (() -> Void)) { + self.backgroundCompletionHandler = backgroundCompletionHandler + } + + func backgroundDownloadsComplete() { + logger.info("Triggering background session completion handler") + + backgroundCompletionHandler?() + backgroundCompletionHandler = nil + } } From c8e53f70b269b236618582310b9b6465f40fa5a8 Mon Sep 17 00:00:00 2001 From: William Boles Date: Mon, 21 Apr 2025 15:25:54 +0100 Subject: [PATCH 5/5] Better comments --- .../Application/AppDelegate.swift | 4 ++-- .../Application/BackgroundTransferRevised_ExampleApp.swift | 2 +- .../BackgroundDownload/BackgroundDownloadService.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BackgroundTransferRevised-Example/Application/AppDelegate.swift b/BackgroundTransferRevised-Example/Application/AppDelegate.swift index 8be131f..26b0d04 100644 --- a/BackgroundTransferRevised-Example/Application/AppDelegate.swift +++ b/BackgroundTransferRevised-Example/Application/AppDelegate.swift @@ -8,8 +8,8 @@ import UIKit import OSLog -class AppDelegate: NSObject, UIApplicationDelegate { - // MARK: - Background +class AppDelegate: NSObject, UIApplicationDelegate { + // MARK: - UIApplicationDelegate func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, diff --git a/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift index 47a4bdb..c60761a 100644 --- a/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift +++ b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift @@ -30,7 +30,7 @@ struct BackgroundTransferRevised_ExampleApp: App { logger.info("Files will be downloaded to: \(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteString)") - //Exit app to test restoring app from a terminated state. Comment out to test restoring app from a suspended state. + //Exit app to test restoring app from a terminated state. Task { logger.info("Simulating app termination by exit(0)") diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift index 01e828b..67c88c4 100644 --- a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift @@ -67,7 +67,7 @@ actor BackgroundDownloadService { self.backgroundCompletionHandler = backgroundCompletionHandler } - func backgroundDownloadsComplete() { + private func backgroundDownloadsComplete() { logger.info("Triggering background session completion handler") backgroundCompletionHandler?()