diff --git a/BackgroundTransfer-Example.xcodeproj/project.pbxproj b/BackgroundTransfer-Example.xcodeproj/project.pbxproj index 4e70beb..e260560 100644 --- a/BackgroundTransfer-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransfer-Example.xcodeproj/project.pbxproj @@ -3,30 +3,22 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 54; 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 */; }; - 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 */; }; + 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 */; }; + 438F85472D6E241D00AF956D /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85442D6E241D00AF956D /* ImageLoader.swift */; }; + 438F85482D6E241D00AF956D /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85452D6E241D00AF956D /* NetworkService.swift */; }; + 438F85492D6E241D00AF956D /* BackgroundDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,9 +32,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,18 +41,13 @@ 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 = ""; }; - 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 = ""; }; + 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 = ""; }; + 438F85442D6E241D00AF956D /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; + 438F85452D6E241D00AF956D /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,23 +68,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 = ( @@ -123,8 +90,8 @@ isa = PBXGroup; children = ( 3DEAF26C20947086004FE44E /* Application */, - 3DEAF28520948C8A004FE44E /* Data */, 3DEAF29A20948C8A004FE44E /* Extensions */, + 438F85432D6E241D00AF956D /* Network */, 3DEAF26F20947086004FE44E /* Resources */, 3DEAF27120947086004FE44E /* Storyboards */, 3DEAF26A20947086004FE44E /* ViewControllers */, @@ -144,7 +111,7 @@ 3DEAF26A20947086004FE44E /* ViewControllers */ = { isa = PBXGroup; children = ( - 3D7762AD2099E82600BCCA3A /* Gallery */, + 43896B762D6906CE00FF34F8 /* Cats */, ); path = ViewControllers; sourceTree = ""; @@ -183,95 +150,48 @@ path = Images; sourceTree = ""; }; - 3DEAF28520948C8A004FE44E /* Data */ = { - isa = PBXGroup; - children = ( - C2CA0B3120CC0415005B3846 /* Downloader */, - 3DEAF28A20948C8A004FE44E /* Managers */, - 3DEAF29620948C8A004FE44E /* Model */, - 3DEAF28620948C8A004FE44E /* Parsers */, - 3DEAF29020948C8A004FE44E /* Requests */, - ); - 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 */ = { + 438F85432D6E241D00AF956D /* Network */ = { isa = PBXGroup; children = ( - C2CA0B3220CC0415005B3846 /* BackgroundDownloader.swift */, - C2D4C8D920D80DB200C740CA /* BackgroundDownloaderContext.swift */, - C2D4C8D720D805A400C740CA /* DownloadItem.swift */, + 438F85442D6E241D00AF956D /* ImageLoader.swift */, + 438F85452D6E241D00AF956D /* NetworkService.swift */, + 438F85462D6E241D00AF956D /* BackgroundDownloadService.swift */, ); - path = Downloader; + path = Network; sourceTree = ""; }; /* End PBXGroup section */ @@ -318,12 +238,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 +255,7 @@ }; 3DA8112020946F5E007F6272 = { CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; }; }; @@ -381,22 +304,14 @@ isa = PBXSourcesBuildPhase; 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 */, - C2CA0B3420CC0415005B3846 /* BackgroundDownloader.swift in Sources */, - 3DEAF2A220948C8A004FE44E /* GalleryDataManager.swift in Sources */, - 3DEAF2A420948C8A004FE44E /* RequestConfig.swift in Sources */, + 43896B7A2D6906CE00FF34F8 /* CatCollectionViewCell.swift in Sources */, 3DEAF27720947086004FE44E /* AppDelegate.swift in Sources */, + 438F85472D6E241D00AF956D /* ImageLoader.swift in Sources */, + 438F85492D6E241D00AF956D /* BackgroundDownloadService.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 */, - C2D4C8D820D805A400C740CA /* DownloadItem.swift in Sources */, + 43896B7B2D6906CE00FF34F8 /* CatsViewController.swift in Sources */, + 43896B7D2D6909A600FF34F8 /* CatsViewModel.swift in Sources */, + 438F85482D6E241D00AF956D /* NetworkService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -464,6 +379,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 +391,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 +406,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 +442,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 +454,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 +463,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 +478,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 +496,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 +514,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 +532,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 @@ - - - - - - - - + + + + + + - - Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } @@ -27,11 +28,8 @@ 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") - - if let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { - debugPrint("Gallery assets will be saved to: \(documentsPath)") - } + os_log(.info, "Simulating app termination by exit(0)") + exit(0) } } @@ -51,6 +49,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Background func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { - BackgroundDownloader.shared.backgroundCompletionHandler = completionHandler + BackgroundDownloadService.shared.backgroundCompletionHandler = completionHandler } } diff --git a/BackgroundTransfer-Example/Application/Info.plist b/BackgroundTransfer-Example/Application/Info.plist index 0f2a09a..b0f82f7 100644 --- a/BackgroundTransfer-Example/Application/Info.plist +++ b/BackgroundTransfer-Example/Application/Info.plist @@ -21,7 +21,10 @@ LSRequiresIPhoneOS UIBackgroundModes - + + fetch + processing + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift deleted file mode 100644 index fa4fd48..0000000 --- a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloader.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// BackgroundDownloader.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 02/05/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation -import UIKit - -class BackgroundDownloader: NSObject { - - var backgroundCompletionHandler: (() -> Void)? - - private let fileManager = FileManager.default - private let context = BackgroundDownloaderContext() - private var session: URLSession! - - // 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)") - downloadItem.foregroundCompletionHandler = completionHandler - } else { - print("Scheduling to download: \(remoteURL)") - - let downloadItem = DownloadItem(remoteURL: remoteURL, filePathURL: filePathURL) - 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.resume() - } - } -} - -// MARK: - URLSessionDelegate - -extension BackgroundDownloader: URLSessionDelegate { - - func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { - DispatchQueue.main.async { - self.backgroundCompletionHandler?() - self.backgroundCompletionHandler = nil - } - } -} - -// MARK: - URLSessionDownloadDelegate - -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 { - return - } - - print("Downloaded: \(downloadItem.remoteURL)") - - do { - try fileManager.moveItem(at: location, to: downloadItem.filePathURL) - - downloadItem.foregroundCompletionHandler?(.success(downloadItem.filePathURL)) - } catch { - downloadItem.foregroundCompletionHandler?(.failure(APIError.invalidData)) - } - - context.deleteDownloadItem(downloadItem) - } -} diff --git a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift b/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift deleted file mode 100644 index 07a407d..0000000 --- a/BackgroundTransfer-Example/Data/Downloader/BackgroundDownloaderContext.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// BackgroundDownloaderContext.swift -// BackgroundTransfer-Example -// -// Created by Boles, William (Developer) on 18/06/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -class BackgroundDownloaderContext { - - private var inMemoryDownloadItems: [URL: DownloadItem] = [:] - private let userDefaults = UserDefaults.standard - - // 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 - } - - private func loadDownloadItemFromStorage(withURL url: URL) -> DownloadItem? { - guard let encodedData = userDefaults.object(forKey: url.path) as? Data else { - return nil - } - - let downloadItem = try? JSONDecoder().decode(DownloadItem.self, from: encodedData) - return downloadItem - } - - // MARK: - Save - - func saveDownloadItem(_ downloadItem: DownloadItem) { - inMemoryDownloadItems[downloadItem.remoteURL] = downloadItem - - let encodedData = try? JSONEncoder().encode(downloadItem) - userDefaults.set(encodedData, forKey: downloadItem.remoteURL.path) - userDefaults.synchronize() - } - - // MARK: - Delete - - func deleteDownloadItem(_ downloadItem: DownloadItem) { - inMemoryDownloadItems[downloadItem.remoteURL] = nil - userDefaults.removeObject(forKey: downloadItem.remoteURL.path) - userDefaults.synchronize() - } -} diff --git a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift b/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift deleted file mode 100644 index 71e8154..0000000 --- a/BackgroundTransfer-Example/Data/Downloader/DownloadItem.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// BackgroundDownloadItem.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 30/05/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import Foundation - -typealias ForegroundDownloadCompletionHandler = ((_ result: DataRequestResult) -> Void) - -class DownloadItem: Codable { - - let remoteURL: URL - let filePathURL: URL - var foregroundCompletionHandler: ForegroundDownloadCompletionHandler? - - private enum CodingKeys: String, CodingKey { - case remoteURL - case filePathURL - } - - // MARK: - Init - - init(remoteURL: URL, filePathURL: URL) { - self.remoteURL = remoteURL - self.filePathURL = filePathURL - } -} 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/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 b1ffe22..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 = "" - 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/Network/BackgroundDownloadService.swift b/BackgroundTransfer-Example/Network/BackgroundDownloadService.swift new file mode 100644 index 0000000..44f8a61 --- /dev/null +++ b/BackgroundTransfer-Example/Network/BackgroundDownloadService.swift @@ -0,0 +1,139 @@ +// +// BackgroundDownloadService.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 02/05/2018. +// Copyright © 2018 William Boles. All rights reserved. +// + +import Foundation +import os + +enum BackgroundDownloadServiceError: Error { + case missingInstructionsError + case fileSystemError(_ underlyingError: Error) + case networkError(_ underlyingError: Error) + case unknownError +} + +class BackgroundDownloadService: NSObject { + var backgroundCompletionHandler: (() -> Void)? + + private var session: URLSession! + + private var foregroundCompletionHandlers = [String: ((result: Result) -> ())]() + + private var userDefaults = UserDefaults.standard + + private let queue = DispatchQueue(label: "com.williamboles.background.download.service") + + // MARK: - Singleton + + static let shared = BackgroundDownloadService() + + // MARK: - Init + + private override init() { + super.init() + + let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session") + configuration.sessionSendsLaunchEvents = true + let session = URLSession(configuration: configuration, + delegate: self, + delegateQueue: nil) + self.session = session + } + + // MARK: - Download + + func download(from remoteURL: URL, + saveDownloadTo localURL: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + queue.async { [weak self] in + os_log(.info, "Scheduling to download: %{public}@", remoteURL.absoluteString) + + self?.foregroundCompletionHandlers[remoteURL.absoluteString] = completionHandler + self?.userDefaults.set(localURL, forKey: remoteURL.absoluteString) + + let task = self?.session.downloadTask(with: remoteURL) + task?.earliestBeginDate = Date().addingTimeInterval(2) // Remove this in production, the delay was added for demonstration purposes only + task?.resume() + } + } +} + +// MARK: - URLSessionDownloadDelegate + +extension BackgroundDownloadService: URLSessionDownloadDelegate { + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + queue.sync { + guard let originalRequestURL = downloadTask.originalRequest?.url?.absoluteString else { + os_log(.error, "Unexpected nil URL") + // Unable to call the closure here as we use originalRequestURL as the key to retrieve the closure + + return + } + + defer { + self.foregroundCompletionHandlers[originalRequestURL] = nil + self.userDefaults.removeObject(forKey: originalRequestURL) + } + + os_log(.info, "Downloaded: %{public}@", originalRequestURL) + + let foregroundCompletionHandler = self.foregroundCompletionHandlers[originalRequestURL] + + guard let saveDownloadToURL = self.userDefaults.url(forKey: originalRequestURL) else { + os_log(.error, "Unable to find existing download item for: %{public}@", originalRequestURL) + foregroundCompletionHandler?(.failure(.missingInstructionsError)) + + return + } + + do { + try FileManager.default.moveItem(at: location, + to: saveDownloadToURL) + + foregroundCompletionHandler?(.success(saveDownloadToURL)) + } catch { + foregroundCompletionHandler?(.failure(.fileSystemError(error))) + } + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + queue.async { [weak self] in + guard let originalRequestURL = task.originalRequest?.url?.absoluteString else { + os_log(.error, "Unexpected nil URL") + + return + } + + defer { + self?.foregroundCompletionHandlers[originalRequestURL] = nil + self?.userDefaults.removeObject(forKey: originalRequestURL) + } + + os_log(.info, "Download failed for: %{public}@", originalRequestURL) + + let foregroundCompletionHandler = self?.foregroundCompletionHandlers[originalRequestURL] + + guard let error = error else { + os_log(.error, "Unknown error caused download to fail for: %{public}@", originalRequestURL) + foregroundCompletionHandler?(.failure(.unknownError)) + + return + } + + foregroundCompletionHandler?(.failure(.networkError(error))) + } + } + + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + DispatchQueue.main.async { + // needs to be called on the main queue + self.backgroundCompletionHandler?() + self.backgroundCompletionHandler = nil + } + } +} diff --git a/BackgroundTransfer-Example/Network/ImageLoader.swift b/BackgroundTransfer-Example/Network/ImageLoader.swift new file mode 100644 index 0000000..4516ca1 --- /dev/null +++ b/BackgroundTransfer-Example/Network/ImageLoader.swift @@ -0,0 +1,72 @@ +// +// ImageLoader.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 21/02/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import UIKit + +enum ImageLoaderError: Error { + case missingData + case invalidImageData +} + +class ImageLoader { + private let backgroundDownloader = BackgroundDownloadService.shared + + // MARK: - Load + + func loadImage(name: String, + url: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + DispatchQueue.global(qos: .background).async { [weak self] in + let fileManager = FileManager.default + let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectoryURL = paths[0] + let localImageURL = documentsDirectoryURL.appendingPathComponent(name) + + if fileManager.fileExists(atPath: localImageURL.path) { + self?.loadLocalImage(localImageURL: localImageURL, + completionHandler: completionHandler) + } else { + self?.loadRemoteImage(remoteImageURL: url, + localImageURL: localImageURL, + completionHandler: completionHandler) + } + } + } + + private func loadLocalImage(localImageURL: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + guard let imageData = try? Data(contentsOf: localImageURL) else { + completionHandler(.failure(ImageLoaderError.missingData)) + return + } + + DispatchQueue.main.async { + guard let image = UIImage(data: imageData) else { + completionHandler(.failure(ImageLoaderError.invalidImageData)) + return + } + completionHandler(.success(image)) + } + } + + private func loadRemoteImage(remoteImageURL: URL, + localImageURL: URL, + completionHandler: @escaping ((_ result: Result) -> ())) { + backgroundDownloader.download(from: remoteImageURL, + saveDownloadTo: localImageURL) { [weak self] result in + switch result { + case let .success(url): + self?.loadLocalImage(localImageURL: url, + completionHandler: completionHandler) + case let .failure(error): + completionHandler(.failure(error)) + } + } + } +} diff --git a/BackgroundTransfer-Example/Network/NetworkService.swift b/BackgroundTransfer-Example/Network/NetworkService.swift new file mode 100644 index 0000000..c3b30b1 --- /dev/null +++ b/BackgroundTransfer-Example/Network/NetworkService.swift @@ -0,0 +1,69 @@ +// +// 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 APIKey = "live_yzNvM2rsrxvWpSwtsAWzbSiGoGW175yNLmnO1u5Fh5GMFxbZ9l4C01t9BcP2v6WQ" + + assert(!APIKey.isEmpty, "Replace this empty string with your API key from: https://thecatapi.com/") + + let limitQueryItem = URLQueryItem(name: "limit", value: "50") + 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(APIKey, 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/Storyboards/Base.lproj/Main.storyboard b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard index bbf26d3..11f8ddc 100644 --- a/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard +++ b/BackgroundTransfer-Example/Storyboards/Base.lproj/Main.storyboard @@ -1,111 +1,108 @@ - - - - + + - + + + - + - - - + + - + - + - + - - + + - - - + + + - - - - + + + + - + - + - + - - - - - - - + + + + + + + + - + - - + + - - + + - - - - - - + + + + + + - - - - - - - - + - - + + - + - + + + + diff --git a/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewController.swift b/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewController.swift new file mode 100644 index 0000000..b279451 --- /dev/null +++ b/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewController.swift @@ -0,0 +1,59 @@ +// +// CatsViewController.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 28/04/2018. +// Copyright © 2018 William Boles. All rights reserved. +// + +import UIKit +import os + +class CatsViewController: UIViewController { + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var loadingActivityIndicatorView: UIActivityIndicatorView! + + private let viewModel = CatsViewModel() + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + loadingActivityIndicatorView.startAnimating() + viewModel.retrieveCats { + self.loadingActivityIndicatorView.stopAnimating() + self.collectionView.reloadData() + } + } +} + +extension CatsViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel.numberOfCats() + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CatCollectionViewCell.className, for: indexPath) as? CatCollectionViewCell else { + fatalError("Expected cell of type: \(CatCollectionViewCell.className)") + } + + viewModel.loadCatImage(at: indexPath) { (image, imageIndexPath) in + guard let cellCurrentIndexPath = collectionView.indexPath(for: cell), + cellCurrentIndexPath == imageIndexPath else { + return + } + + cell.catImageView.image = image + } + + return cell + } +} + +extension CatsViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cellWidth = (view.frame.size.width - 12.0)/3.0 + return CGSize(width: cellWidth, height: cellWidth) + } +} diff --git a/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewModel.swift b/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewModel.swift new file mode 100644 index 0000000..b5cd3ff --- /dev/null +++ b/BackgroundTransfer-Example/ViewControllers/Cats/CatsViewModel.swift @@ -0,0 +1,60 @@ +// +// CatsViewModel.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 21/02/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import os +import UIKit + +class CatsViewModel { + private var cats: [Cat] = [] + + private let networkService = NetworkService() + private let imageLoader = ImageLoader() + + // MARK: - Retrieval + + func retrieveCats(completion: @escaping (() -> ())) { + networkService.retrieveCats { [weak self] result in + DispatchQueue.main.async { + switch result { + case let .success(cats): + self?.cats = cats + + completion() + case let .failure(error): + // TODO: Handle + os_log(.error, "Error when retrieving json: %{public}@", error.localizedDescription) + } + } + } + } + + func numberOfCats() -> Int { + return cats.count + } + + // MARK: - Image + + func loadCatImage(at indexPath: IndexPath, + completion: @escaping (((UIImage, IndexPath)) -> ())) { + let cat = cats[indexPath.row] + + imageLoader.loadImage(name: cat.id, + url: cat.url) { result in + DispatchQueue.main.async { + switch result { + case let .success(image): + completion((image, indexPath)) + case let .failure(error): + // TODO: Handle + os_log(.error, "Error when loading image: %{public}@", error.localizedDescription) + } + } + } + } +} diff --git a/BackgroundTransfer-Example/ViewControllers/Cats/Cells/CatCollectionViewCell.swift b/BackgroundTransfer-Example/ViewControllers/Cats/Cells/CatCollectionViewCell.swift new file mode 100644 index 0000000..46bb9b1 --- /dev/null +++ b/BackgroundTransfer-Example/ViewControllers/Cats/Cells/CatCollectionViewCell.swift @@ -0,0 +1,22 @@ +// +// CatCollectionViewCell.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 28/04/2018. +// Copyright © 2018 William Boles. All rights reserved. +// + +import UIKit +import os + +class CatCollectionViewCell: UICollectionViewCell { + @IBOutlet weak var catImageView: UIImageView! + + // MARK: - Reuse + + override func prepareForReuse() { + super.prepareForReuse() + + catImageView.image = UIImage(named: "icon-placeholder") + } +} diff --git a/BackgroundTransfer-Example/ViewControllers/Gallery/Cells/GalleryAssetCollectionViewCell.swift b/BackgroundTransfer-Example/ViewControllers/Gallery/Cells/GalleryAssetCollectionViewCell.swift deleted file mode 100644 index 3919886..0000000 --- a/BackgroundTransfer-Example/ViewControllers/Gallery/Cells/GalleryAssetCollectionViewCell.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// GalleryAssetCollectionViewCell.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import UIKit - -class GalleryAssetCollectionViewCell: UICollectionViewCell { - - @IBOutlet weak var assetImageView: UIImageView! - - private var assetDataManager = GalleryAssetDataManager() - private var asset: GalleryAsset? - - // MARK: - Reuse - - override func prepareForReuse() { - super.prepareForReuse() - - assetImageView.image = UIImage(named: "icon-placeholder") - } - - // MARK: - Configure - - func configure(asset: GalleryAsset) { - self.asset = asset - let image = assetDataManager.load(galleryItemAsset: asset) { [weak self] (result) in - switch result { - case .success(let loadResult): - if loadResult.asset == self?.asset { - self?.assetImageView.image = loadResult.image - } - case .failure(let error): - //TODO: Handle - print(error) - } - } - - if image != nil { - assetImageView.image = image - } - } -} diff --git a/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift b/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift deleted file mode 100644 index cd80135..0000000 --- a/BackgroundTransfer-Example/ViewControllers/Gallery/GalleryViewController.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// GalleryViewController.swift -// BackgroundTransfer-Example -// -// Created by William Boles on 28/04/2018. -// Copyright © 2018 William Boles. All rights reserved. -// - -import UIKit - -class GalleryViewController: UIViewController { - - @IBOutlet weak var collectionView: UICollectionView! - @IBOutlet weak var loadingActivityIndicatorView: UIActivityIndicatorView! - - let dataManager = GalleryDataManager() - var assets = [GalleryAsset]() - let fileManager = FileManager.default - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - retrieveAlbums() - } - - // MARK: - Albums - - func retrieveAlbums() { - loadingActivityIndicatorView.startAnimating() - - dataManager.retrieveGallery(forSearchTerms: "cats") { (searchTerms, result) in - self.loadingActivityIndicatorView.stopAnimating() - - switch result { - case .success(let assets): - self.assets = assets - self.collectionView.reloadData() - case .failure(let error): - //TODO: Handle - print("\(error)") - } - } - } - - // MARK: - Reset - - @IBAction func resetButtonPressed(_ sender: Any) { - loadingActivityIndicatorView.startAnimating() - - for asset in assets { - try? fileManager.removeItem(at: asset.cachedLocalAssetURL()) - } - - assets.removeAll() - collectionView.reloadData() - retrieveAlbums() - } -} - -extension GalleryViewController: UICollectionViewDataSource { - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return assets.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GalleryAssetCollectionViewCell.className, for: indexPath) as? GalleryAssetCollectionViewCell else { - fatalError("Expected cell of type: \(GalleryAssetCollectionViewCell.className)") - } - - let asset = assets[indexPath.row] - - cell.configure(asset: asset) - - return cell - } -} - -extension GalleryViewController: UICollectionViewDelegateFlowLayout { - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - let cellWidth = (view.frame.size.width - 12.0)/3.0 - return CGSize(width: cellWidth, height: cellWidth) - } -} 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 { diff --git a/README.md b/README.md index 08eac26..8f0e06b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![Build](https://github.com/wibosco/BackgroundTransfer-Example/actions/workflows/swift.yml/badge.svg)](https://github.com/wibosco/BackgroundTransfer-Example/actions/workflows/swift.yml) -Swift +Swift [![License](http://img.shields.io/badge/License-MIT-green.svg?style=flat)](https://github.com/wibosco/BackgroundTransfer-Example/blob/main/LICENSE) # BackgroundTransfer-Example An example project looking at how to implement background transfers on iOS as shown in this article - https://williamboles.com/keeping-things-going-when-the-user-leaves-with-urlsession-and-background-transfers/ -In order to run this project, you will need to register with [Imgur](https://api.imgur.com/oauth2/addclient) to get a `client-id` token to access Imgur's API (which the project uses to get its example content). Once you have your `client-id`, add it to the project as the value of the `clientID` property in the `RequestConfig` class and the project should now run. If you have any trouble getting the project to run, please create an issue. +In order to run this project, you will need to register with [TheCatAPI](https://thecatapi.com/) to get a `x-api-key` token to access TheCatAPI's API (which the project uses to get its example content). Once you have your `x-api-key`, add it to the project as the value of the `APIKey` property in the `NetworkService` class and the project should now run. If you have any trouble getting the project to run, please create an issue.