From adfce2f120134f950a111edf9b462e68636f956a Mon Sep 17 00:00:00 2001 From: William Boles Date: Thu, 20 Feb 2025 14:48:34 +0000 Subject: [PATCH 01/10] Added different stores for DownloadItem --- .../project.pbxproj | 50 ++++++++--- .../BackgroundTransfer-Example.xcscheme | 30 +++---- .../Downloader/BackgroundDownloader.swift | 22 ++--- .../BackgroundDownloaderContext.swift | 88 +++++++++++++++---- .../Data/Downloader/DownloadItem.swift | 1 - .../Requests/Abstract/RequestConfig.swift | 2 +- .../Storyboards/Base.lproj/Main.storyboard | 20 ++--- .../Gallery/GalleryViewController.swift | 2 + .../BackgroundTransfer_ExampleTests.swift | 1 + 9 files changed, 140 insertions(+), 76 deletions(-) diff --git a/BackgroundTransfer-Example.xcodeproj/project.pbxproj b/BackgroundTransfer-Example.xcodeproj/project.pbxproj index 4e70beb..244d3b5 100644 --- a/BackgroundTransfer-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransfer-Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -318,12 +318,14 @@ 3DA8110520946F5D007F6272 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0930; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1520; ORGANIZATIONNAME = "William Boles"; TargetAttributes = { 3DA8110C20946F5D007F6272 = { CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -333,6 +335,7 @@ }; 3DA8112020946F5E007F6272 = { CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; }; }; @@ -464,6 +467,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -475,6 +479,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -489,7 +494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -525,6 +530,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -536,6 +542,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -544,10 +551,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; @@ -558,10 +566,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "BackgroundTransfer-Example/Application/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransfer-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -572,10 +584,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "BackgroundTransfer-Example/Application/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransfer-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -586,10 +602,14 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "BackgroundTransfer-ExampleTests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransfer-ExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -600,10 +620,14 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "BackgroundTransfer-ExampleTests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransfer-ExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/BackgroundTransfer-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransfer-Example.xcscheme b/BackgroundTransfer-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransfer-Example.xcscheme index 8017f2d..0d73aff 100644 --- a/BackgroundTransfer-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransfer-Example.xcscheme +++ b/BackgroundTransfer-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransfer-Example.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - + + + + + + - - Void)? private let fileManager = FileManager.default private let context = BackgroundDownloaderContext() - private var session: URLSession! + private lazy var session: URLSession = { + let configuration = URLSessionConfiguration.background(withIdentifier: "background.download.session") + let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + return session + }() // MARK: - Singleton static let shared = BackgroundDownloader() - // MARK: - Init - - private override init() { - super.init() - - let configuration = URLSessionConfiguration.background(withIdentifier: "background.download.session") - session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) - } - // MARK: - Download func download(remoteURL: URL, filePathURL: URL, completionHandler: @escaping ForegroundDownloadCompletionHandler) { if let downloadItem = context.loadDownloadItem(withURL: remoteURL) { - print("Already downloading: \(remoteURL)") + os_log(.info, "Already downloading: %{public}@", remoteURL.absoluteString) downloadItem.foregroundCompletionHandler = completionHandler } else { - print("Scheduling to download: \(remoteURL)") + os_log(.info, "Scheduling to download: %{public}@", remoteURL.absoluteString) let downloadItem = DownloadItem(remoteURL: remoteURL, filePathURL: filePathURL) downloadItem.foregroundCompletionHandler = completionHandler diff --git a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift index 07a407d..177b8c0 100644 --- a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift +++ b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift @@ -8,26 +8,46 @@ import Foundation -class BackgroundDownloaderContext { - - private var inMemoryDownloadItems: [URL: DownloadItem] = [:] - private let userDefaults = UserDefaults.standard +protocol DownloadItemStore { + func loadDownloadItem(withURL url: URL) -> DownloadItem? + func saveDownloadItem(_ downloadItem: DownloadItem) + func deleteDownloadItem(_ downloadItem: DownloadItem) +} + +class MemoryStore: DownloadItemStore { + private var downloadItems: [URL: DownloadItem] = [:] // MARK: - Load func loadDownloadItem(withURL url: URL) -> DownloadItem? { - if let downloadItem = inMemoryDownloadItems[url] { - return downloadItem - } else if let downloadItem = loadDownloadItemFromStorage(withURL: url) { - inMemoryDownloadItems[downloadItem.remoteURL] = downloadItem - - return downloadItem - } - - return nil + return downloadItems[url] } - private func loadDownloadItemFromStorage(withURL url: URL) -> DownloadItem? { + // MARK: - Save + + func saveDownloadItem(_ downloadItem: DownloadItem) { + downloadItems[downloadItem.remoteURL] = downloadItem + } + + // MARK: - Delete + + func deleteDownloadItem(_ downloadItem: DownloadItem) { + downloadItems[downloadItem.remoteURL] = nil + } +} + +class UserDefaultsStore: DownloadItemStore { + private let userDefaults: UserDefaults + + // MARK: - Init + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + // MARK: - Load + + func loadDownloadItem(withURL url: URL) -> DownloadItem? { guard let encodedData = userDefaults.object(forKey: url.path) as? Data else { return nil } @@ -39,18 +59,48 @@ class BackgroundDownloaderContext { // MARK: - Save func saveDownloadItem(_ downloadItem: DownloadItem) { - inMemoryDownloadItems[downloadItem.remoteURL] = downloadItem - - let encodedData = try? JSONEncoder().encode(downloadItem) - userDefaults.set(encodedData, forKey: downloadItem.remoteURL.path) + let data = try? JSONEncoder().encode(downloadItem) + userDefaults.set(data, forKey: downloadItem.remoteURL.path) userDefaults.synchronize() } // MARK: - Delete func deleteDownloadItem(_ downloadItem: DownloadItem) { - inMemoryDownloadItems[downloadItem.remoteURL] = nil userDefaults.removeObject(forKey: downloadItem.remoteURL.path) userDefaults.synchronize() } } + +class BackgroundDownloaderContext { + private let inMemoryStore: DownloadItemStore + private let persistentStore: DownloadItemStore + + // MARK: - Init + + init(inMemoryStore: DownloadItemStore = MemoryStore(), + persistentStore: DownloadItemStore = UserDefaultsStore()) { + self.inMemoryStore = inMemoryStore + self.persistentStore = persistentStore + } + + // MARK: - Load + + func loadDownloadItem(withURL url: URL) -> DownloadItem? { + return inMemoryStore.loadDownloadItem(withURL: url) ?? persistentStore.loadDownloadItem(withURL: url) + } + + // MARK: - Save + + func saveDownloadItem(_ downloadItem: DownloadItem) { + inMemoryStore.saveDownloadItem(downloadItem) + persistentStore.saveDownloadItem(downloadItem) + } + + // MARK: - Delete + + func deleteDownloadItem(_ downloadItem: DownloadItem) { + inMemoryStore.deleteDownloadItem(downloadItem) + persistentStore.deleteDownloadItem(downloadItem) + } +} diff --git a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift b/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift index 71e8154..8f89e82 100644 --- a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift +++ b/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift @@ -11,7 +11,6 @@ import Foundation typealias ForegroundDownloadCompletionHandler = ((_ result: DataRequestResult) -> Void) class DownloadItem: Codable { - let remoteURL: URL let filePathURL: URL var foregroundCompletionHandler: ForegroundDownloadCompletionHandler? diff --git a/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift b/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift index b1ffe22..f71728b 100644 --- a/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift +++ b/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift @@ -23,7 +23,7 @@ class RequestConfig { // MARK: - Init init() { - self.clientID = "" + self.clientID = "c85c6251f7e5303" self.APIHost = "https://api.imgur.com/3" assert(!clientID.isEmpty, "You need to provide a clientID hash, you get this from: https://api.imgur.com/oauth2/addclient") diff --git a/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard index bbf26d3..4970f37 100644 --- a/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard +++ b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -53,15 +51,15 @@ - + - - - - + + + + @@ -77,6 +75,7 @@ + @@ -86,7 +85,6 @@ - diff --git a/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift b/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift index cd80135..3b3d525 100644 --- a/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift +++ b/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift @@ -73,6 +73,8 @@ extension GalleryViewController: UICollectionViewDataSource { let asset = assets[indexPath.row] cell.configure(asset: asset) + cell.layer.borderColor = UIColor.red.cgColor + cell.layer.borderWidth = 4 return cell } diff --git a/BackgroundTransfer-ExampleTests/BackgroundTransfer_ExampleTests.swift b/BackgroundTransfer-ExampleTests/BackgroundTransfer_ExampleTests.swift index 19becae..e3e26ec 100644 --- a/BackgroundTransfer-ExampleTests/BackgroundTransfer_ExampleTests.swift +++ b/BackgroundTransfer-ExampleTests/BackgroundTransfer_ExampleTests.swift @@ -7,6 +7,7 @@ // import XCTest + @testable import BackgroundTransfer_Example class BackgroundTransfer_ExampleTests: XCTestCase { From 4b9642b1bc61848a79b363fb6bf73a482acad5ac Mon Sep 17 00:00:00 2001 From: William Boles Date: Sat, 22 Feb 2025 14:58:54 +0000 Subject: [PATCH 02/10] Switched over to using catapi --- .../project.pbxproj | 136 +++++------------- .../Application/AppDelegate.swift | 7 +- .../Downloader/BackgroundDownloader.swift | 22 +-- .../Data/Downloader/DownloadItem.swift | 13 +- .../Data/ImageLoader.swift | 54 +++++++ .../Managers/GalleryAssetDataManager.swift | 65 --------- .../Data/Managers/GalleryDataManager.swift | 59 -------- .../Data/Model/GalleryAsset.swift | 23 --- .../Data/NetworkService.swift | 65 +++++++++ .../Data/Parsers/Abstract/Parser.swift | 18 --- .../Data/Parsers/GalleryParser.swift | 88 ------------ .../Requests/Abstract/RequestConfig.swift | 31 ---- .../Abstract/URLRequest+HTTPBody.swift | 23 --- .../Requests/Abstract/URLRequestFactory.swift | 47 ------ .../Requests/GalleryURLRequestFactory.swift | 21 --- .../Storyboards/Base.lproj/Main.storyboard | 30 ++-- .../Cats/CatsViewController.swift | 58 ++++++++ .../ViewControllers/Cats/CatsViewModel.swift | 58 ++++++++ .../Cats/Cells/CatCollectionViewCell.swift | 45 ++++++ .../GalleryAssetCollectionViewCell.swift | 46 ------ .../Gallery/GalleryViewController.swift | 89 ------------ 21 files changed, 351 insertions(+), 647 deletions(-) create mode 100644 BackgroundTransfer-Example/Data/ImageLoader.swift delete mode 100644 BackgroundTransfer-Example/Data/Managers/GalleryAssetDataManager.swift delete mode 100644 BackgroundTransfer-Example/Data/Managers/GalleryDataManager.swift delete mode 100644 BackgroundTransfer-Example/Data/Model/GalleryAsset.swift create mode 100644 BackgroundTransfer-Example/Data/NetworkService.swift delete mode 100644 BackgroundTransfer-Example/Data/Parsers/Abstract/Parser.swift delete mode 100644 BackgroundTransfer-Example/Data/Parsers/GalleryParser.swift delete mode 100644 BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift delete mode 100644 BackgroundTransfer-Example/Data/Requests/Abstract/URLRequest+HTTPBody.swift delete mode 100644 BackgroundTransfer-Example/Data/Requests/Abstract/URLRequestFactory.swift delete mode 100644 BackgroundTransfer-Example/Data/Requests/GalleryURLRequestFactory.swift create mode 100644 BackgroundTransfer-Example/ViewControllers/Cats/CatsViewController.swift create mode 100644 BackgroundTransfer-Example/ViewControllers/Cats/CatsViewModel.swift create mode 100644 BackgroundTransfer-Example/ViewControllers/Cats/Cells/CatCollectionViewCell.swift delete mode 100644 BackgroundTransfer-Example/ViewControllers/Gallery/Cells/GalleryAssetCollectionViewCell.swift delete mode 100644 BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift diff --git a/BackgroundTransfer-Example.xcodeproj/project.pbxproj b/BackgroundTransfer-Example.xcodeproj/project.pbxproj index 244d3b5..d9a94e3 100644 --- a/BackgroundTransfer-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransfer-Example.xcodeproj/project.pbxproj @@ -7,23 +7,17 @@ objects = { /* Begin PBXBuildFile section */ - 3D7762AC2099E49F00BCCA3A /* GalleryAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7762AB2099E49F00BCCA3A /* GalleryAsset.swift */; }; - 3D7762B12099E82600BCCA3A /* GalleryAssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7762AF2099E82600BCCA3A /* GalleryAssetCollectionViewCell.swift */; }; - 3D7762B22099E82600BCCA3A /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7762B02099E82600BCCA3A /* GalleryViewController.swift */; }; 3DA8112620946F5E007F6272 /* BackgroundTransfer_ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA8112520946F5E007F6272 /* BackgroundTransfer_ExampleTests.swift */; }; 3DEAF27720947086004FE44E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF26D20947086004FE44E /* AppDelegate.swift */; }; 3DEAF27A20947086004FE44E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3DEAF27220947086004FE44E /* LaunchScreen.storyboard */; }; 3DEAF27B20947086004FE44E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3DEAF27420947086004FE44E /* Main.storyboard */; }; 3DEAF27E20948B9A004FE44E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3DEAF27D20948B9A004FE44E /* Assets.xcassets */; }; - 3DEAF29D20948C8A004FE44E /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF28820948C8A004FE44E /* Parser.swift */; }; - 3DEAF29E20948C8A004FE44E /* GalleryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF28920948C8A004FE44E /* GalleryParser.swift */; }; - 3DEAF2A120948C8A004FE44E /* GalleryAssetDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF28E20948C8A004FE44E /* GalleryAssetDataManager.swift */; }; - 3DEAF2A220948C8A004FE44E /* GalleryDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF28F20948C8A004FE44E /* GalleryDataManager.swift */; }; - 3DEAF2A320948C8A004FE44E /* URLRequest+HTTPBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF29220948C8A004FE44E /* URLRequest+HTTPBody.swift */; }; - 3DEAF2A420948C8A004FE44E /* RequestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF29320948C8A004FE44E /* RequestConfig.swift */; }; - 3DEAF2A520948C8A004FE44E /* URLRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF29420948C8A004FE44E /* URLRequestFactory.swift */; }; - 3DEAF2A620948C8A004FE44E /* GalleryURLRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF29520948C8A004FE44E /* GalleryURLRequestFactory.swift */; }; 3DEAF2AA20948C8A004FE44E /* NSObject+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEAF29C20948C8A004FE44E /* NSObject+Name.swift */; }; + 43896B752D68FE9800FF34F8 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43896B742D68FE9800FF34F8 /* NetworkService.swift */; }; + 43896B7A2D6906CE00FF34F8 /* CatCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43896B782D6906CE00FF34F8 /* CatCollectionViewCell.swift */; }; + 43896B7B2D6906CE00FF34F8 /* CatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43896B792D6906CE00FF34F8 /* CatsViewController.swift */; }; + 43896B7D2D6909A600FF34F8 /* CatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43896B7C2D6909A600FF34F8 /* CatsViewModel.swift */; }; + 43896B7F2D6914A000FF34F8 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43896B7E2D6914A000FF34F8 /* ImageLoader.swift */; }; C2CA0B3420CC0415005B3846 /* BackgroundDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CA0B3220CC0415005B3846 /* BackgroundDownloader.swift */; }; C2D4C8D820D805A400C740CA /* DownloadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D4C8D720D805A400C740CA /* DownloadItem.swift */; }; C2D4C8DA20D80DB200C740CA /* BackgroundDownloaderContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D4C8D920D80DB200C740CA /* BackgroundDownloaderContext.swift */; }; @@ -40,9 +34,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 3D7762AB2099E49F00BCCA3A /* GalleryAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryAsset.swift; sourceTree = ""; }; - 3D7762AF2099E82600BCCA3A /* GalleryAssetCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryAssetCollectionViewCell.swift; sourceTree = ""; }; - 3D7762B02099E82600BCCA3A /* GalleryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = ""; }; 3DA8110D20946F5D007F6272 /* BackgroundTransfer-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BackgroundTransfer-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3DA8112120946F5E007F6272 /* BackgroundTransfer-ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BackgroundTransfer-ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 3DA8112520946F5E007F6272 /* BackgroundTransfer_ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTransfer_ExampleTests.swift; sourceTree = ""; }; @@ -52,15 +43,12 @@ 3DEAF27320947086004FE44E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 3DEAF27520947086004FE44E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 3DEAF27D20948B9A004FE44E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 3DEAF28820948C8A004FE44E /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; - 3DEAF28920948C8A004FE44E /* GalleryParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryParser.swift; sourceTree = ""; }; - 3DEAF28E20948C8A004FE44E /* GalleryAssetDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryAssetDataManager.swift; sourceTree = ""; }; - 3DEAF28F20948C8A004FE44E /* GalleryDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryDataManager.swift; sourceTree = ""; }; - 3DEAF29220948C8A004FE44E /* URLRequest+HTTPBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+HTTPBody.swift"; sourceTree = ""; }; - 3DEAF29320948C8A004FE44E /* RequestConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestConfig.swift; sourceTree = ""; }; - 3DEAF29420948C8A004FE44E /* URLRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestFactory.swift; sourceTree = ""; }; - 3DEAF29520948C8A004FE44E /* GalleryURLRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryURLRequestFactory.swift; sourceTree = ""; }; 3DEAF29C20948C8A004FE44E /* NSObject+Name.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+Name.swift"; sourceTree = ""; }; + 43896B742D68FE9800FF34F8 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 43896B782D6906CE00FF34F8 /* CatCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatCollectionViewCell.swift; sourceTree = ""; }; + 43896B792D6906CE00FF34F8 /* CatsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatsViewController.swift; sourceTree = ""; }; + 43896B7C2D6909A600FF34F8 /* CatsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatsViewModel.swift; sourceTree = ""; }; + 43896B7E2D6914A000FF34F8 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; C2CA0B3220CC0415005B3846 /* BackgroundDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownloader.swift; sourceTree = ""; }; C2D4C8D720D805A400C740CA /* DownloadItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadItem.swift; sourceTree = ""; }; C2D4C8D920D80DB200C740CA /* BackgroundDownloaderContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloaderContext.swift; sourceTree = ""; }; @@ -84,23 +72,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 3D7762AD2099E82600BCCA3A /* Gallery */ = { - isa = PBXGroup; - children = ( - 3D7762AE2099E82600BCCA3A /* Cells */, - 3D7762B02099E82600BCCA3A /* GalleryViewController.swift */, - ); - path = Gallery; - sourceTree = ""; - }; - 3D7762AE2099E82600BCCA3A /* Cells */ = { - isa = PBXGroup; - children = ( - 3D7762AF2099E82600BCCA3A /* GalleryAssetCollectionViewCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; 3DA8110420946F5D007F6272 = { isa = PBXGroup; children = ( @@ -144,7 +115,7 @@ 3DEAF26A20947086004FE44E /* ViewControllers */ = { isa = PBXGroup; children = ( - 3D7762AD2099E82600BCCA3A /* Gallery */, + 43896B762D6906CE00FF34F8 /* Cats */, ); path = ViewControllers; sourceTree = ""; @@ -187,81 +158,44 @@ isa = PBXGroup; children = ( C2CA0B3120CC0415005B3846 /* Downloader */, - 3DEAF28A20948C8A004FE44E /* Managers */, - 3DEAF29620948C8A004FE44E /* Model */, - 3DEAF28620948C8A004FE44E /* Parsers */, - 3DEAF29020948C8A004FE44E /* Requests */, + 43896B742D68FE9800FF34F8 /* NetworkService.swift */, + 43896B7E2D6914A000FF34F8 /* ImageLoader.swift */, ); path = Data; sourceTree = ""; }; - 3DEAF28620948C8A004FE44E /* Parsers */ = { - isa = PBXGroup; - children = ( - 3DEAF28720948C8A004FE44E /* Abstract */, - 3DEAF28920948C8A004FE44E /* GalleryParser.swift */, - ); - path = Parsers; - sourceTree = ""; - }; - 3DEAF28720948C8A004FE44E /* Abstract */ = { - isa = PBXGroup; - children = ( - 3DEAF28820948C8A004FE44E /* Parser.swift */, - ); - path = Abstract; - sourceTree = ""; - }; - 3DEAF28A20948C8A004FE44E /* Managers */ = { - isa = PBXGroup; - children = ( - 3DEAF28E20948C8A004FE44E /* GalleryAssetDataManager.swift */, - 3DEAF28F20948C8A004FE44E /* GalleryDataManager.swift */, - ); - path = Managers; - sourceTree = ""; - }; - 3DEAF29020948C8A004FE44E /* Requests */ = { - isa = PBXGroup; - children = ( - 3DEAF29120948C8A004FE44E /* Abstract */, - 3DEAF29520948C8A004FE44E /* GalleryURLRequestFactory.swift */, - ); - path = Requests; - sourceTree = ""; - }; - 3DEAF29120948C8A004FE44E /* Abstract */ = { + 3DEAF29A20948C8A004FE44E /* Extensions */ = { isa = PBXGroup; children = ( - 3DEAF29220948C8A004FE44E /* URLRequest+HTTPBody.swift */, - 3DEAF29320948C8A004FE44E /* RequestConfig.swift */, - 3DEAF29420948C8A004FE44E /* URLRequestFactory.swift */, + 3DEAF29B20948C8A004FE44E /* NSObject */, ); - path = Abstract; + path = Extensions; sourceTree = ""; }; - 3DEAF29620948C8A004FE44E /* Model */ = { + 3DEAF29B20948C8A004FE44E /* NSObject */ = { isa = PBXGroup; children = ( - 3D7762AB2099E49F00BCCA3A /* GalleryAsset.swift */, + 3DEAF29C20948C8A004FE44E /* NSObject+Name.swift */, ); - path = Model; + path = NSObject; sourceTree = ""; }; - 3DEAF29A20948C8A004FE44E /* Extensions */ = { + 43896B762D6906CE00FF34F8 /* Cats */ = { isa = PBXGroup; children = ( - 3DEAF29B20948C8A004FE44E /* NSObject */, + 43896B772D6906CE00FF34F8 /* Cells */, + 43896B792D6906CE00FF34F8 /* CatsViewController.swift */, + 43896B7C2D6909A600FF34F8 /* CatsViewModel.swift */, ); - path = Extensions; + path = Cats; sourceTree = ""; }; - 3DEAF29B20948C8A004FE44E /* NSObject */ = { + 43896B772D6906CE00FF34F8 /* Cells */ = { isa = PBXGroup; children = ( - 3DEAF29C20948C8A004FE44E /* NSObject+Name.swift */, + 43896B782D6906CE00FF34F8 /* CatCollectionViewCell.swift */, ); - path = NSObject; + path = Cells; sourceTree = ""; }; C2CA0B3120CC0415005B3846 /* Downloader */ = { @@ -385,20 +319,14 @@ buildActionMask = 2147483647; files = ( C2D4C8DA20D80DB200C740CA /* BackgroundDownloaderContext.swift in Sources */, - 3DEAF29D20948C8A004FE44E /* Parser.swift in Sources */, - 3D7762B22099E82600BCCA3A /* GalleryViewController.swift in Sources */, - 3D7762B12099E82600BCCA3A /* GalleryAssetCollectionViewCell.swift in Sources */, - 3DEAF2A320948C8A004FE44E /* URLRequest+HTTPBody.swift in Sources */, + 43896B7A2D6906CE00FF34F8 /* CatCollectionViewCell.swift in Sources */, + 43896B7F2D6914A000FF34F8 /* ImageLoader.swift in Sources */, C2CA0B3420CC0415005B3846 /* BackgroundDownloader.swift in Sources */, - 3DEAF2A220948C8A004FE44E /* GalleryDataManager.swift in Sources */, - 3DEAF2A420948C8A004FE44E /* RequestConfig.swift in Sources */, 3DEAF27720947086004FE44E /* AppDelegate.swift in Sources */, 3DEAF2AA20948C8A004FE44E /* NSObject+Name.swift in Sources */, - 3DEAF29E20948C8A004FE44E /* GalleryParser.swift in Sources */, - 3DEAF2A120948C8A004FE44E /* GalleryAssetDataManager.swift in Sources */, - 3D7762AC2099E49F00BCCA3A /* GalleryAsset.swift in Sources */, - 3DEAF2A520948C8A004FE44E /* URLRequestFactory.swift in Sources */, - 3DEAF2A620948C8A004FE44E /* GalleryURLRequestFactory.swift in Sources */, + 43896B7B2D6906CE00FF34F8 /* CatsViewController.swift in Sources */, + 43896B752D68FE9800FF34F8 /* NetworkService.swift in Sources */, + 43896B7D2D6909A600FF34F8 /* CatsViewModel.swift in Sources */, C2D4C8D820D805A400C740CA /* DownloadItem.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/BackgroundTransfer-Example/Application/AppDelegate.swift b/BackgroundTransfer-Example/Application/AppDelegate.swift index 0cb07e3..d974f66 100644 --- a/BackgroundTransfer-Example/Application/AppDelegate.swift +++ b/BackgroundTransfer-Example/Application/AppDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import os @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -15,7 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Lifecycle - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } @@ -27,10 +28,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidEnterBackground(_ application: UIApplication) { //Exit app to test restoring app from a terminated state. Comment out to test restoring app from a suspended state. DispatchQueue.main.asyncAfter(deadline: .now()) { - print("App is about to quit") + os_log(.info, "App is about to quit") if let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { - debugPrint("Gallery assets will be saved to: \(documentsPath)") + os_log(.info, "Gallery assets will be saved to: %{public}@", documentsPath) } exit(0) } diff --git a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift index 709f8e7..6cf07ad 100644 --- a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift +++ b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift @@ -26,19 +26,22 @@ class BackgroundDownloader: NSObject { // MARK: - Download - func download(remoteURL: URL, filePathURL: URL, completionHandler: @escaping ForegroundDownloadCompletionHandler) { + func download(remoteURL: URL, + localURL: URL, + completionHandler: @escaping ForegroundDownloadCompletionHandler) { if let downloadItem = context.loadDownloadItem(withURL: remoteURL) { os_log(.info, "Already downloading: %{public}@", remoteURL.absoluteString) + downloadItem.foregroundCompletionHandler = completionHandler } else { os_log(.info, "Scheduling to download: %{public}@", remoteURL.absoluteString) - let downloadItem = DownloadItem(remoteURL: remoteURL, filePathURL: filePathURL) + let downloadItem = DownloadItem(remoteURL: remoteURL, localURL: localURL) downloadItem.foregroundCompletionHandler = completionHandler context.saveDownloadItem(downloadItem) let task = session.downloadTask(with: remoteURL) - task.earliestBeginDate = Date().addingTimeInterval(20) // Added a delay for demonstration purposes only + task.earliestBeginDate = Date().addingTimeInterval(2) // Added a delay for demonstration purposes only task.resume() } } @@ -61,18 +64,19 @@ extension BackgroundDownloader: URLSessionDelegate { extension BackgroundDownloader: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - guard let originalRequestURL = downloadTask.originalRequest?.url, let downloadItem = context.loadDownloadItem(withURL: originalRequestURL) else { + guard let originalRequestURL = downloadTask.originalRequest?.url, + let downloadItem = context.loadDownloadItem(withURL: originalRequestURL) else { return } - - print("Downloaded: \(downloadItem.remoteURL)") + + os_log(.info, "Downloaded: %{public}@", downloadItem.remoteURL.absoluteString) do { - try fileManager.moveItem(at: location, to: downloadItem.filePathURL) + try fileManager.moveItem(at: location, to: downloadItem.localURL) - downloadItem.foregroundCompletionHandler?(.success(downloadItem.filePathURL)) + downloadItem.foregroundCompletionHandler?(.success(downloadItem.localURL)) } catch { - downloadItem.foregroundCompletionHandler?(.failure(APIError.invalidData)) + downloadItem.foregroundCompletionHandler?(.failure(error)) } context.deleteDownloadItem(downloadItem) diff --git a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift b/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift index 8f89e82..5cef289 100644 --- a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift +++ b/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift @@ -8,22 +8,23 @@ import Foundation -typealias ForegroundDownloadCompletionHandler = ((_ result: DataRequestResult) -> Void) +typealias ForegroundDownloadCompletionHandler = ((_ result: Result) -> Void) -class DownloadItem: Codable { +class DownloadItem: Codable { // TODO: Make into a struct? let remoteURL: URL - let filePathURL: URL + let localURL: URL var foregroundCompletionHandler: ForegroundDownloadCompletionHandler? private enum CodingKeys: String, CodingKey { case remoteURL - case filePathURL + case localURL } // MARK: - Init - init(remoteURL: URL, filePathURL: URL) { + init(remoteURL: URL, + localURL: URL) { self.remoteURL = remoteURL - self.filePathURL = filePathURL + self.localURL = localURL } } diff --git a/BackgroundTransfer-Example/Data/ImageLoader.swift b/BackgroundTransfer-Example/Data/ImageLoader.swift new file mode 100644 index 0000000..eb1e893 --- /dev/null +++ b/BackgroundTransfer-Example/Data/ImageLoader.swift @@ -0,0 +1,54 @@ +// +// ImageLoader.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 21/02/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import UIKit + +class ImageLoader { + private let backgroundDownloader = BackgroundDownloader() + + // MARK: - Load + + func loadImage(name: String, + url: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectoryURL = paths[0] + let localImageURL = documentsDirectoryURL.appendingPathComponent(name) + + if let image = loadLocalImage(localImageURL: localImageURL) { + completionHandler(.success(image)) + } else { + loadRemoteImage(remoteImageURL: url, + localImageURL: localImageURL, + completionHandler: completionHandler) + } + } + + private func loadLocalImage(localImageURL: URL) -> UIImage? { + return UIImage(contentsOfFile: localImageURL.path) + } + + private func loadRemoteImage(remoteImageURL: URL, + localImageURL: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + backgroundDownloader.download(remoteURL: remoteImageURL, + localURL: localImageURL) { [weak self] result in + switch result { + case let .success(url): + guard let image = self?.loadLocalImage(localImageURL: url) else { + // TODO: Handle error + return + } + completionHandler(.success(image)) + case let .failure(error): + completionHandler(.failure(error)) + } + } + } +} diff --git a/BackgroundTransfer-Example/Data/Managers/GalleryAssetDataManager.swift b/BackgroundTransfer-Example/Data/Managers/GalleryAssetDataManager.swift deleted file mode 100644 index 9eeb880..0000000 --- a/BackgroundTransfer-Example/Data/Managers/GalleryAssetDataManager.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// GalleryAssetDataManager.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation -import UIKit - -enum DataRequestResult { - case success(T) - case failure(Error) -} - -struct LoadAssetResult: Equatable { - let asset: GalleryAsset - let image: UIImage -} - -class GalleryAssetDataManager { - - // MARK: - GalleryItem - - func load(galleryItemAsset asset: GalleryAsset, remoteLoadHandler: @escaping ((_ result: DataRequestResult) -> ())) -> UIImage? { - if let image = UIImage(contentsOfFile: asset.cachedLocalAssetURL().path) { - return image - } else { - remotelyLoadAsset(asset, remoteLoadHandler: remoteLoadHandler) - } - - return nil - } - - private func remotelyLoadAsset(_ asset: GalleryAsset, remoteLoadHandler: @escaping ((_ result: DataRequestResult) -> ())) { - let downloader = BackgroundDownloader.shared - - downloader.download(remoteURL: asset.url, filePathURL: asset.cachedLocalAssetURL()) { (result) in - switch result { - case .success(let url): - var retrievedData: Data? = nil - do { - retrievedData = try Data(contentsOf: url) - } catch { - remoteLoadHandler(.failure(APIError.invalidData)) - } - - guard let imageData = retrievedData, let image = UIImage(data: imageData) else { - remoteLoadHandler(.failure(APIError.invalidData)) - return - } - - let loadResult = LoadAssetResult(asset: asset, image: image) - let dataRequestResult = DataRequestResult.success(loadResult) - - DispatchQueue.main.async { - remoteLoadHandler(dataRequestResult) - } - case .failure(let error): - remoteLoadHandler(.failure(error)) - } - } - } -} diff --git a/BackgroundTransfer-Example/Data/Managers/GalleryDataManager.swift b/BackgroundTransfer-Example/Data/Managers/GalleryDataManager.swift deleted file mode 100644 index ff763f9..0000000 --- a/BackgroundTransfer-Example/Data/Managers/GalleryDataManager.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// GalleryDataManager.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -class GalleryDataManager { - - let urlRequestFactory: GalleryURLRequestFactory - let session: URLSession - - // MARK: - Init - - init(session: URLSession = URLSession.shared, urlRequestFactory: GalleryURLRequestFactory = GalleryURLRequestFactory()) { - self.session = session - self.urlRequestFactory = urlRequestFactory - } - - // MARK: - List - - func retrieveGallery(forSearchTerms searchTerms: String, completionHandler: @escaping ((_ searchTerms: String, _ result: DataRequestResult<[GalleryAsset]>) -> ())) { - let request = urlRequestFactory.requestToRetrieveGallerySearchResults(for: searchTerms) - - let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in - - if error != nil || data == nil { - DispatchQueue.main.async { - if error != nil { - completionHandler(searchTerms, DataRequestResult.failure(error!)) - } else { - completionHandler(searchTerms, DataRequestResult.failure(APIError.missingData)) - } - } - return - } - - do { - let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: Any] - - let parser = GalleryParser() - let galleryAlbums = parser.parseResponse(json) - - DispatchQueue.main.async { - completionHandler(searchTerms, DataRequestResult.success(galleryAlbums)) - } - } catch { - DispatchQueue.main.async { - completionHandler(searchTerms, DataRequestResult.failure(APIError.serialization)) - } - } - } - - task.resume() - } -} diff --git a/BackgroundTransfer-Example/Data/Model/GalleryAsset.swift b/BackgroundTransfer-Example/Data/Model/GalleryAsset.swift deleted file mode 100644 index 4894878..0000000 --- a/BackgroundTransfer-Example/Data/Model/GalleryAsset.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// GalleryAsset.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 02/05/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -struct GalleryAsset: Equatable { - - let id: String - let url: URL - - // MARK: - Location - - func cachedLocalAssetURL() -> URL { - let cacheURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last! - let fileName = url.deletingPathExtension().lastPathComponent - return cacheURL.appendingPathComponent(fileName) - } -} diff --git a/BackgroundTransfer-Example/Data/NetworkService.swift b/BackgroundTransfer-Example/Data/NetworkService.swift new file mode 100644 index 0000000..d0c0fbe --- /dev/null +++ b/BackgroundTransfer-Example/Data/NetworkService.swift @@ -0,0 +1,65 @@ +// +// NetworkService.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 21/02/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import os + +struct Cat: Decodable, Equatable { + let id: String + let url: URL +} + +enum NetworkServiceError: Error { + case networkError + case unexpectedStatusCode + case decodingErrror +} + +class NetworkService { + // MARK: - Cats + + func retrieveCats(completionHandler: @escaping ((Result<[Cat], Error>) -> ())) { + let limitQueryItem = URLQueryItem(name: "limit", value: "100") + let sizeQueryItem = URLQueryItem(name: "size", value: "thumb") + + let queryItems = [limitQueryItem, sizeQueryItem] + + var components = URLComponents() + components.scheme = "https" + components.host = "api.thecatapi.com" + components.path = "/v1/images/search" + components.queryItems = queryItems + + var urlRequest = URLRequest(url: components.url!) + urlRequest.httpMethod = "GET" + urlRequest.addValue("live_yzNvM2rsrxvWpSwtsAWzbSiGoGW175yNLmnO1u5Fh5GMFxbZ9l4C01t9BcP2v6WQ", forHTTPHeaderField: "x-api-key") + + os_log(.error, "Retrieving cats...") + + let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in + guard let data = data, let response = response else { + completionHandler(.failure(NetworkServiceError.networkError)) + return + } + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + completionHandler(.failure(NetworkServiceError.unexpectedStatusCode)) + return + } + + guard let cats = try? JSONDecoder().decode([Cat].self, from: data) else { + completionHandler(.failure(NetworkServiceError.decodingErrror)) + return + } + + os_log(.error, "Cats successfully retrieved!") + completionHandler(.success(cats)) + } + task.resume() + } +} diff --git a/BackgroundTransfer-Example/Data/Parsers/Abstract/Parser.swift b/BackgroundTransfer-Example/Data/Parsers/Abstract/Parser.swift deleted file mode 100644 index 1fe1e8a..0000000 --- a/BackgroundTransfer-Example/Data/Parsers/Abstract/Parser.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Parser.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -class Parser { - - // MARK: - Parse - - func parseResponse(_ response: [String: Any]) -> T { - fatalError("Subclass needs to override this method") - } -} diff --git a/BackgroundTransfer-Example/Data/Parsers/GalleryParser.swift b/BackgroundTransfer-Example/Data/Parsers/GalleryParser.swift deleted file mode 100644 index 817d263..0000000 --- a/BackgroundTransfer-Example/Data/Parsers/GalleryParser.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// GalleryParser.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -class GalleryParser: Parser<[GalleryAsset]> { - - // MARK: - Parse - - override func parseResponse(_ response: [String: Any]) -> [GalleryAsset] { - var assets = [GalleryAsset]() - - guard let itemsResponse = response["data"] as? [[String: Any]] else { - return assets - } - - for itemResponse in itemsResponse { - if let parsedAssets = parseItem(itemResponse) { - if parsedAssets.count > 0 { - assets.append(contentsOf: parsedAssets) - } - } - } - - return assets - } - - private func parseItem(_ itemResponse: [String: Any]) -> [GalleryAsset]? { - guard let isAlbum = itemResponse["is_album"] as? Bool else { - return nil - } - - if isAlbum { - return parseItemAlbum(itemResponse) - } else { - return parseItemImage(itemResponse) - } - } - - func parseItemImage(_ itemResponse: [String: Any]) -> [GalleryAsset]? { - guard let imageURLString = itemResponse["link"] as? String, - let imageURL = URL(string: imageURLString), - isImageJPEG(imageURL: imageURL) - else { - return nil - } - - let asset = GalleryAsset(id: fileName(forURL: imageURL), url: imageURL) - - return [asset] - } - - func parseItemAlbum(_ itemResponse: [String: Any]) -> [GalleryAsset]? { - guard let imageResponses = itemResponse["images"] as? [[String: Any]] - else { - return nil - } - - var assets = [GalleryAsset]() - - for imageResponse in imageResponses { - if let linkURLString = imageResponse["link"] as? String { - if let linkURL = URL(string: linkURLString) { - if isImageJPEG(imageURL: linkURL) { - let asset = GalleryAsset(id: fileName(forURL: linkURL), url: linkURL) - - assets.append(asset) - } - } - } - } - - return assets - } - - func fileName(forURL url: URL) -> String { - return url.deletingPathExtension().lastPathComponent - } - - func isImageJPEG(imageURL: URL) -> Bool { - return imageURL.pathExtension.lowercased() == "jpg" - } -} diff --git a/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift b/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift deleted file mode 100644 index f71728b..0000000 --- a/BackgroundTransfer-Example/Data/Requests/Abstract/RequestConfig.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// RequestConfig.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -enum HTTPRequestMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case delete = "DELETE" -} - -class RequestConfig { - - let clientID: String - let APIHost: String - - // MARK: - Init - - init() { - self.clientID = "c85c6251f7e5303" - self.APIHost = "https://api.imgur.com/3" - - assert(!clientID.isEmpty, "You need to provide a clientID hash, you get this from: https://api.imgur.com/oauth2/addclient") - } -} diff --git a/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequest+HTTPBody.swift b/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequest+HTTPBody.swift deleted file mode 100644 index 0c18245..0000000 --- a/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequest+HTTPBody.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// URLRequest+HTTPBody.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -extension URLRequest { - - // MARK: - JSON - - mutating func setJSONParameters(_ parameters: [String: Any]?) { - guard let parameters = parameters else { - httpBody = nil - return - } - - httpBody = try! JSONSerialization.data(withJSONObject: parameters, options: JSONSerialization.WritingOptions(rawValue: 0)) - } -} diff --git a/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequestFactory.swift b/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequestFactory.swift deleted file mode 100644 index 244bf44..0000000 --- a/BackgroundTransfer-Example/Data/Requests/Abstract/URLRequestFactory.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// URLRequestFactory.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -enum APIError: Error { - case unknown - case missingData - case serialization - case invalidData -} - -class URLRequestFactory { - - private let config: RequestConfig - - // MARK: - Init - - init(config: RequestConfig = RequestConfig()) { - self.config = config - } - - // MARK: - Factory - - func baseRequest(endPoint: String) -> URLRequest { - let stringURL = "\(config.APIHost)/\(endPoint)" - let encodedStringURL = stringURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - let url = URL(string: encodedStringURL!)! - - var request = URLRequest(url: url) - request.addValue("Client-ID \(config.clientID)", forHTTPHeaderField: "Authorization") - - return request - } - - func jsonRequest(endPoint: String) -> URLRequest { - var request = baseRequest(endPoint: endPoint) - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - return request - } -} diff --git a/BackgroundTransfer-Example/Data/Requests/GalleryURLRequestFactory.swift b/BackgroundTransfer-Example/Data/Requests/GalleryURLRequestFactory.swift deleted file mode 100644 index a9ff6e7..0000000 --- a/BackgroundTransfer-Example/Data/Requests/GalleryURLRequestFactory.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GalleryURLRequestFactory.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -class GalleryURLRequestFactory: URLRequestFactory { - - // MARK: - Retrieval - - func requestToRetrieveGallerySearchResults(for searchTerms: String) -> URLRequest { - var request = jsonRequest(endPoint: "gallery/search/?q_all=\(searchTerms)&q_type=jpg") - request.httpMethod = HTTPRequestMethod.get.rawValue - - return request - } -} diff --git a/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard index 4970f37..1984672 100644 --- a/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard +++ b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -25,10 +25,10 @@ - + - + @@ -43,26 +43,26 @@ - + - - + + - - - - + + + + - + @@ -71,8 +71,8 @@ -