Skip to content

Commit 05e2029

Browse files
committed
Implemented store
1 parent a08b13e commit 05e2029

File tree

5 files changed

+136
-86
lines changed

5 files changed

+136
-86
lines changed

BackgroundTransfer-Example.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
438F85472D6E241D00AF956D /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85442D6E241D00AF956D /* ImageLoader.swift */; };
2020
438F85482D6E241D00AF956D /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85452D6E241D00AF956D /* NetworkService.swift */; };
2121
438F85492D6E241D00AF956D /* BackgroundDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */; };
22+
43DAF0CE2D8C75D3005900E7 /* BackgroundDownloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DAF0CD2D8C75D3005900E7 /* BackgroundDownloadStore.swift */; };
2223
/* End PBXBuildFile section */
2324

2425
/* Begin PBXContainerItemProxy section */
@@ -48,6 +49,7 @@
4849
438F85442D6E241D00AF956D /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = "<group>"; };
4950
438F85452D6E241D00AF956D /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
5051
438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = "<group>"; };
52+
43DAF0CD2D8C75D3005900E7 /* BackgroundDownloadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadStore.swift; sourceTree = "<group>"; };
5153
/* End PBXFileReference section */
5254

5355
/* Begin PBXFrameworksBuildPhase section */
@@ -190,6 +192,7 @@
190192
438F85442D6E241D00AF956D /* ImageLoader.swift */,
191193
438F85452D6E241D00AF956D /* NetworkService.swift */,
192194
438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */,
195+
43DAF0CD2D8C75D3005900E7 /* BackgroundDownloadStore.swift */,
193196
);
194197
path = Network;
195198
sourceTree = "<group>";
@@ -307,6 +310,7 @@
307310
43896B7A2D6906CE00FF34F8 /* CatCollectionViewCell.swift in Sources */,
308311
3DEAF27720947086004FE44E /* AppDelegate.swift in Sources */,
309312
438F85472D6E241D00AF956D /* ImageLoader.swift in Sources */,
313+
43DAF0CE2D8C75D3005900E7 /* BackgroundDownloadStore.swift in Sources */,
310314
438F85492D6E241D00AF956D /* BackgroundDownloadService.swift in Sources */,
311315
3DEAF2AA20948C8A004FE44E /* NSObject+Name.swift in Sources */,
312316
43896B7B2D6906CE00FF34F8 /* CatsViewController.swift in Sources */,

BackgroundTransfer-Example/Application/AppDelegate.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4848

4949
// MARK: - Background
5050

51-
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
52-
BackgroundDownloadService.shared.backgroundCompletionHandler = completionHandler
51+
func application(_ application: UIApplication,
52+
handleEventsForBackgroundURLSession identifier: String,
53+
completionHandler: @escaping () -> Void) {
54+
BackgroundDownloadService().backgroundCompletionHandler = completionHandler
5355
}
5456
}

BackgroundTransfer-Example/Network/BackgroundDownloadService.swift

Lines changed: 75 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,29 @@
99
import Foundation
1010
import 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+
2022
class 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

6664
extension 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) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// BackgroundDownloadStore.swift
3+
// BackgroundTransfer-Example
4+
//
5+
// Created by William Boles on 20/03/2025.
6+
// Copyright © 2025 William Boles. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import os
11+
12+
class BackgroundDownloadStore {
13+
private var inMemoryStore = [String: BackgroundDownloadCompletion]()
14+
private let persistentStore = UserDefaults.standard
15+
16+
private let queue = DispatchQueue(label: "com.williamboles.background.download.service")
17+
18+
// MARK: - Store
19+
20+
func storeMetadata(from fromURL: URL,
21+
to toURL: URL,
22+
completionHandler: @escaping BackgroundDownloadCompletion) {
23+
queue.async { [weak self] in
24+
self?.inMemoryStore[fromURL.absoluteString] = completionHandler
25+
self?.persistentStore.set(toURL, forKey: fromURL.absoluteString)
26+
}
27+
}
28+
29+
// MARK: - Retrieve
30+
31+
func retrieveMetadata(for forURL: URL) -> (URL?, BackgroundDownloadCompletion?) {
32+
return queue.sync { [weak self] in
33+
let key = forURL.absoluteString
34+
35+
let toURL = self?.persistentStore.url(forKey: key)
36+
let completionHandler = self?.inMemoryStore[key]
37+
38+
return (toURL, completionHandler)
39+
}
40+
}
41+
42+
// MARK: - Remove
43+
44+
func removeMetadata(for forURL: URL) {
45+
queue.async { [weak self] in
46+
let key = forURL.absoluteString
47+
48+
self?.inMemoryStore[key] = nil
49+
self?.persistentStore.removeObject(forKey: key)
50+
}
51+
}
52+
}

BackgroundTransfer-Example/Network/NetworkService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class NetworkService {
2424
// MARK: - Cats
2525

2626
func retrieveCats(completionHandler: @escaping ((Result<[Cat], Error>) -> ())) {
27-
let APIKey = ""
27+
let APIKey = "live_yzNvM2rsrxvWpSwtsAWzbSiGoGW175yNLmnO1u5Fh5GMFxbZ9l4C01t9BcP2v6WQ"
2828

2929
assert(!APIKey.isEmpty, "Replace this empty string with your API key from: https://thecatapi.com/")
3030

0 commit comments

Comments
 (0)