99import Foundation
1010import os
1111
12- enum BackgroundDownloadServiceError : Error {
12+ enum BackgroundDownloadError : Error {
1313 case missingInstructionsError
1414 case fileSystemError( _ underlyingError: Error )
1515 case networkError( _ underlyingError: Error ? )
1616 case unexpectedResponseError
1717 case unexpectedStatusCode
1818}
1919
20+ typealias BackgroundDownloadCompletion = ( _ result: Result < URL , BackgroundDownloadError > ) -> ( )
21+
2022class BackgroundDownloadService : NSObject {
2123 var backgroundCompletionHandler : ( ( ) -> Void ) ?
2224
2325 private var session : URLSession !
24-
25- private var foregroundCompletionHandlers = [ String : ( ( result: Result < URL , BackgroundDownloadServiceError > ) -> ( ) ) ] ( )
26-
27- private let queue = DispatchQueue ( label: " com.williamboles.background.download.service " )
26+ private let store = BackgroundDownloadStore ( )
2827
2928 // MARK: - Singleton
3029
3130 static let shared = BackgroundDownloadService ( )
3231
3332 // MARK: - Init
3433
35- private override init ( ) {
34+ override init ( ) {
3635 super. init ( )
3736
3837 let configuration = URLSessionConfiguration . background ( withIdentifier: " com.williamboles.background.download.session " )
@@ -47,99 +46,92 @@ class BackgroundDownloadService: NSObject {
4746
4847 func download( from remoteURL: URL ,
4948 saveDownloadTo localURL: URL ,
50- completionHandler: @escaping ( ( _ result: Result < URL , BackgroundDownloadServiceError > ) -> ( ) ) ) {
51- queue. async { [ weak self] in
52- os_log ( . info, " Scheduling to download: %{public}@ " , remoteURL. absoluteString)
53-
54- self ? . foregroundCompletionHandlers [ remoteURL. absoluteString] = completionHandler
55- UserDefaults . standard. set ( localURL, forKey: remoteURL. absoluteString)
56-
57- let task = self ? . session. downloadTask ( with: remoteURL)
58- task? . earliestBeginDate = Date ( ) . addingTimeInterval ( 2 ) // Remove this in production, the delay was added for demonstration purposes only
59- task? . resume ( )
60- }
49+ completionHandler: @escaping BackgroundDownloadCompletion ) {
50+ os_log ( . info, " Scheduling to download: %{public}@ " , remoteURL. absoluteString)
51+
52+ store. storeMetadata ( from: remoteURL,
53+ to: localURL,
54+ completionHandler: completionHandler)
55+
56+ let task = session. downloadTask ( with: remoteURL)
57+ task. earliestBeginDate = Date ( ) . addingTimeInterval ( 2 ) // Remove this in production, the delay was added for demonstration purposes only
58+ task. resume ( )
6159 }
6260}
6361
6462// MARK: - URLSessionDownloadDelegate
6563
6664extension BackgroundDownloadService : URLSessionDownloadDelegate {
67- func urlSession( _ session: URLSession ,
65+ func urlSession( _ session: URLSession ,
6866 downloadTask: URLSessionDownloadTask ,
6967 didFinishDownloadingTo location: URL ) {
70- queue. sync {
71- guard let originalRequestURL = downloadTask. originalRequest? . url? . absoluteString else {
72- os_log ( . error, " Unexpected nil URL " )
73- // Unable to call the closure here as we use originalRequestURL as the key to retrieve the closure
74-
75- return
76- }
77-
78- defer {
79- self . foregroundCompletionHandlers [ originalRequestURL] = nil
80- UserDefaults . standard. removeObject ( forKey: originalRequestURL)
81- }
82-
83- os_log ( . info, " Download request completed for: %{public}@ " , originalRequestURL)
84-
85- let foregroundCompletionHandler = self . foregroundCompletionHandlers [ originalRequestURL]
86-
87- guard let response = downloadTask. response as? HTTPURLResponse else {
88- os_log ( . error, " Unexpected response for: %{public}@ " , originalRequestURL)
89- foregroundCompletionHandler ? ( . failure( . unexpectedResponseError) )
90- return
91- }
92-
93- guard response. statusCode == 200 else {
94- os_log ( . error, " Unexpected status code of: %{public}d, for: %{public}@ " , response. statusCode, originalRequestURL)
95- foregroundCompletionHandler ? ( . failure( . unexpectedStatusCode) )
96- return
97- }
98-
99- os_log ( . info, " Download successful for: %{public}@ " , originalRequestURL)
100-
101- guard let saveDownloadToURL = UserDefaults . standard. url ( forKey: originalRequestURL) else {
102- os_log ( . error, " Unable to find existing download item for: %{public}@ " , originalRequestURL)
103- foregroundCompletionHandler ? ( . failure( . missingInstructionsError) )
104-
105- return
106- }
68+ guard let fromURL = downloadTask. originalRequest? . url else {
69+ os_log ( . error, " Unexpected nil URL " )
70+ // Unable to call the closure here as we use originalRequestURL as the key to retrieve the closure
71+ return
72+ }
73+
74+ defer {
75+ store. removeMetadata ( for: fromURL)
76+ }
77+
78+ let fromURLAbsoluteString = fromURL. absoluteString
79+
80+ os_log ( . info, " Download request completed for: %{public}@ " , fromURLAbsoluteString)
81+
82+ let ( toURL, completionHandler) = store. retrieveMetadata ( for: fromURL)
83+ guard let toURL else {
84+ os_log ( . error, " Unable to find existing download item for: %{public}@ " , fromURLAbsoluteString)
85+ completionHandler ? ( . failure( . missingInstructionsError) )
86+ return
87+ }
88+
89+ guard let response = downloadTask. response as? HTTPURLResponse else {
90+ os_log ( . error, " Unexpected response for: %{public}@ " , fromURLAbsoluteString)
91+ completionHandler ? ( . failure( . unexpectedResponseError) )
92+ return
93+ }
94+
95+ guard response. statusCode == 200 else {
96+ os_log ( . error, " Unexpected status code of: %{public}d, for: %{public}@ " , response. statusCode, fromURLAbsoluteString)
97+ completionHandler ? ( . failure( . unexpectedStatusCode) )
98+ return
99+ }
100+
101+ os_log ( . info, " Download successful for: %{public}@ " , fromURLAbsoluteString)
102+
103+ do {
104+ try FileManager . default. moveItem ( at: location,
105+ to: toURL)
107106
108- do {
109- try FileManager . default. moveItem ( at: location,
110- to: saveDownloadToURL)
111-
112- foregroundCompletionHandler ? ( . success( saveDownloadToURL) )
113- } catch {
114- foregroundCompletionHandler ? ( . failure( . fileSystemError( error) ) )
115- }
107+ completionHandler ? ( . success( toURL) )
108+ } catch {
109+ completionHandler ? ( . failure( . fileSystemError( error) ) )
116110 }
117111 }
118112
119113 func urlSession( _ session: URLSession ,
120114 task: URLSessionTask ,
121115 didCompleteWithError error: Error ? ) {
122- queue. async { [ weak self] in
123- guard let error = error else {
124- return
125- }
126-
127- guard let originalRequestURL = task. originalRequest? . url? . absoluteString else {
128- os_log ( . error, " Unexpected nil URL " )
129-
130- return
131- }
132-
133- defer {
134- self ? . foregroundCompletionHandlers [ originalRequestURL] = nil
135- UserDefaults . standard. removeObject ( forKey: originalRequestURL)
136- }
137-
138- os_log ( . info, " Download failed for: %{public}@ " , originalRequestURL)
139-
140- let foregroundCompletionHandler = self ? . foregroundCompletionHandlers [ originalRequestURL]
141- foregroundCompletionHandler ? ( . failure( . networkError( error) ) )
116+ guard let error = error else {
117+ return
118+ }
119+
120+ guard let fromURL = task. originalRequest? . url else {
121+ os_log ( . error, " Unexpected nil URL " )
122+ return
142123 }
124+
125+ defer {
126+ store. removeMetadata ( for: fromURL)
127+ }
128+
129+ let fromURLAbsoluteString = fromURL. absoluteString
130+
131+ os_log ( . info, " Download failed for: %{public}@ " , fromURLAbsoluteString)
132+
133+ let ( _, completionHandler) = store. retrieveMetadata ( for: fromURL)
134+ completionHandler ? ( . failure( . networkError( error) ) )
143135 }
144136
145137 func urlSessionDidFinishEvents( forBackgroundURLSession session: URLSession ) {
0 commit comments