Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions BackgroundTransferRevised-Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +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 */; };
4395E6632DB00E8300637803 /* BackgroundDownloadTaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.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 */; };
Expand Down Expand Up @@ -43,8 +42,7 @@
437CD5132D94C66C00A909A6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = "<group>"; };
4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadMetaStore.swift; sourceTree = "<group>"; };
4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadTaskStore.swift; sourceTree = "<group>"; };
4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegator.swift; sourceTree = "<group>"; };
4395E6642DB00ECD00637803 /* BackgroundDownloadDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegate.swift; sourceTree = "<group>"; };
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 = "<group>"; };
43D1CF5F2D80860E00AC1ED9 /* BackgroundTransferRevised-ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BackgroundTransferRevised-ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -110,10 +108,9 @@
4395E65D2DB00E4100637803 /* BackgroundDownload */ = {
isa = PBXGroup;
children = (
4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */,
4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */,
4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */,
4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */,
4395E6642DB00ECD00637803 /* BackgroundDownloadDelegate.swift */,
);
path = BackgroundDownload;
sourceTree = "<group>";
Expand Down Expand Up @@ -300,8 +297,7 @@
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 */,
4395E6652DB00ECD00637803 /* BackgroundDownloadDelegate.swift in Sources */,
43D1CF842D808A5000AC1ED9 /* ImageLoader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
19 changes: 4 additions & 15 deletions BackgroundTransferRevised-Example/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,13 @@ 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")

// MARK: - Background
// MARK: - UIApplicationDelegate

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Task {
logger.info("Simulating app termination by exit(0)")

exit(0)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// BackgroundDownloadDelegator.swift
// BackgroundTransferRevised-Example
//
// Created by William Boles on 16/04/2025.
//

import Foundation
import OSLog

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
private let urlSessionDidFinishEventsCompletionHandler: (@Sendable () -> Void)

// MARK: - Init

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

func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
guard let fromURL = downloadTask.originalRequest?.url else {
logger.error("Unexpected nil URL for download task.")
return
}

logger.info("Download request completed for: \(fromURL.absoluteString)")

let tempLocation = FileManager.default.temporaryDirectory.appendingPathComponent(location.lastPathComponent)
try? FileManager.default.moveItem(at: location,
to: tempLocation)

processingGroup.enter()
metaStore.retrieveMetadata(key: fromURL.absoluteString) { [weak self] metadata in
defer {
self?.metaStore.removeMetadata(key: fromURL.absoluteString)
self?.processingGroup.leave()
}

guard let metadata else {
self?.logger.error("Unable to find existing download item for: \(fromURL.absoluteString)")
return
}

guard let response = downloadTask.response as? HTTPURLResponse,
response.statusCode == 200 else {
self?.logger.error("Unexpected response for: \(fromURL.absoluteString)")
metadata.continuation?.resume(throwing: BackgroundDownloadError.serverError(downloadTask.response))
return
}

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 {
self?.logger.error("File system error while moving file: \(error.localizedDescription)")
metadata.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error))
}
}
}

func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
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
defer {
self?.metaStore.removeMetadata(key: fromURL.absoluteString)
self?.processingGroup.leave()
}

metadata?.continuation?.resume(throwing: BackgroundDownloadError.clientError(error))
}
}

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
logger.info("Did finish events for background session")

processingGroup.notify(queue: .global()) { [weak self] in
self?.logger.info("Processing group has finished")

self?.urlSessionDidFinishEventsCompletionHandler()
}
}
}

This file was deleted.

Loading