diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1e6f17e..e26e907 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -12,7 +12,15 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: List available Xcode versions + run: ls /Applications | grep Xcode + - name: Show current version of Xcode + run: xcodebuild -version + - name: Set up Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.1.0.app/Contents/Developer + - name: Show current version of Xcode + run: xcodebuild -version - name: Install Bundle run: bundle install - name: Run unit tests diff --git a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj index 849bc75..f05f77c 100644 --- a/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj +++ b/BackgroundTransferRevised-Example.xcodeproj/project.pbxproj @@ -3,15 +3,24 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ - 43D1CF532D80860C00AC1ED9 /* BackgroundTransferRevised_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF522D80860C00AC1ED9 /* BackgroundTransferRevised_ExampleApp.swift */; }; - 43D1CF552D80860C00AC1ED9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF542D80860C00AC1ED9 /* ContentView.swift */; }; - 43D1CF572D80860E00AC1ED9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF562D80860E00AC1ED9 /* Assets.xcassets */; }; + 436CC0972D808CC500F9E4E2 /* CatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436CC0952D808CC400F9E4E2 /* CatsViewModel.swift */; }; + 436CC0982D808CC500F9E4E2 /* CatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436CC0962D808CC400F9E4E2 /* CatsView.swift */; }; + 436CC09B2D8091D300F9E4E2 /* GridItem+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436CC09A2D8091D300F9E4E2 /* GridItem+Layout.swift */; }; + 437CD5102D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CD50D2D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift */; }; + 437CD5112D94C62800A909A6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CD50E2D94C62800A909A6 /* AppDelegate.swift */; }; + 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */; }; + 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */; }; + 4395E6632DB00E8300637803 /* BackgroundDownloadTaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */; }; + 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */; }; 43D1CF5A2D80860E00AC1ED9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */; }; 43D1CF642D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF632D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift */; }; + 43D1CF832D808A5000AC1ED9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43D1CF7E2D808A5000AC1ED9 /* Assets.xcassets */; }; + 43D1CF842D808A5000AC1ED9 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF802D808A5000AC1ED9 /* ImageLoader.swift */; }; + 43D1CF852D808A5000AC1ED9 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D1CF812D808A5000AC1ED9 /* NetworkService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -25,13 +34,24 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 436CC0952D808CC400F9E4E2 /* CatsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatsViewModel.swift; sourceTree = ""; }; + 436CC0962D808CC400F9E4E2 /* CatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatsView.swift; sourceTree = ""; }; + 436CC09A2D8091D300F9E4E2 /* GridItem+Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GridItem+Layout.swift"; sourceTree = ""; }; + 437CD50D2D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTransferRevised_ExampleApp.swift; sourceTree = ""; }; + 437CD50E2D94C62800A909A6 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 437CD50F2D94C62800A909A6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 437CD5132D94C66C00A909A6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadService.swift; sourceTree = ""; }; + 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadMetaStore.swift; sourceTree = ""; }; + 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadTaskStore.swift; sourceTree = ""; }; + 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDownloadDelegator.swift; sourceTree = ""; }; 43D1CF4F2D80860C00AC1ED9 /* BackgroundTransferRevised-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BackgroundTransferRevised-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 43D1CF522D80860C00AC1ED9 /* BackgroundTransferRevised_ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTransferRevised_ExampleApp.swift; sourceTree = ""; }; - 43D1CF542D80860C00AC1ED9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 43D1CF562D80860E00AC1ED9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43D1CF592D80860E00AC1ED9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 43D1CF5F2D80860E00AC1ED9 /* BackgroundTransferRevised-ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BackgroundTransferRevised-ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 43D1CF632D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTransferRevised_ExampleTests.swift; sourceTree = ""; }; + 43D1CF7E2D808A5000AC1ED9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 43D1CF802D808A5000AC1ED9 /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; + 43D1CF812D808A5000AC1ED9 /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -52,6 +72,52 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 436CC0932D808CC400F9E4E2 /* Features */ = { + isa = PBXGroup; + children = ( + 436CC0942D808CC400F9E4E2 /* Cats */, + ); + path = Features; + sourceTree = ""; + }; + 436CC0942D808CC400F9E4E2 /* Cats */ = { + isa = PBXGroup; + children = ( + 436CC0952D808CC400F9E4E2 /* CatsViewModel.swift */, + 436CC0962D808CC400F9E4E2 /* CatsView.swift */, + ); + path = Cats; + sourceTree = ""; + }; + 436CC0992D8091D300F9E4E2 /* Extensions */ = { + isa = PBXGroup; + children = ( + 436CC09A2D8091D300F9E4E2 /* GridItem+Layout.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 437CD50C2D94C62800A909A6 /* Application */ = { + isa = PBXGroup; + children = ( + 437CD50D2D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift */, + 437CD50E2D94C62800A909A6 /* AppDelegate.swift */, + 437CD50F2D94C62800A909A6 /* Info.plist */, + ); + path = Application; + sourceTree = ""; + }; + 4395E65D2DB00E4100637803 /* BackgroundDownload */ = { + isa = PBXGroup; + children = ( + 4395E6622DB00E8300637803 /* BackgroundDownloadTaskStore.swift */, + 4395E65B2DB00E4100637803 /* BackgroundDownloadService.swift */, + 4395E65C2DB00E4100637803 /* BackgroundDownloadMetaStore.swift */, + 4395E6642DB00ECD00637803 /* BackgroundDownloadDelegator.swift */, + ); + path = BackgroundDownload; + sourceTree = ""; + }; 43D1CF462D80860C00AC1ED9 = { isa = PBXGroup; children = ( @@ -73,9 +139,11 @@ 43D1CF512D80860C00AC1ED9 /* BackgroundTransferRevised-Example */ = { isa = PBXGroup; children = ( - 43D1CF522D80860C00AC1ED9 /* BackgroundTransferRevised_ExampleApp.swift */, - 43D1CF542D80860C00AC1ED9 /* ContentView.swift */, - 43D1CF562D80860E00AC1ED9 /* Assets.xcassets */, + 437CD50C2D94C62800A909A6 /* Application */, + 436CC0992D8091D300F9E4E2 /* Extensions */, + 436CC0932D808CC400F9E4E2 /* Features */, + 43D1CF7F2D808A5000AC1ED9 /* Network */, + 43D1CF7C2D808A5000AC1ED9 /* Resources */, 43D1CF582D80860E00AC1ED9 /* Preview Content */, ); path = "BackgroundTransferRevised-Example"; @@ -92,11 +160,38 @@ 43D1CF622D80860E00AC1ED9 /* BackgroundTransferRevised-ExampleTests */ = { isa = PBXGroup; children = ( + 437CD5132D94C66C00A909A6 /* Info.plist */, 43D1CF632D80860E00AC1ED9 /* BackgroundTransferRevised_ExampleTests.swift */, ); path = "BackgroundTransferRevised-ExampleTests"; sourceTree = ""; }; + 43D1CF7C2D808A5000AC1ED9 /* Resources */ = { + isa = PBXGroup; + children = ( + 43D1CF7D2D808A5000AC1ED9 /* Images */, + ); + path = Resources; + sourceTree = ""; + }; + 43D1CF7D2D808A5000AC1ED9 /* Images */ = { + isa = PBXGroup; + children = ( + 43D1CF7E2D808A5000AC1ED9 /* Assets.xcassets */, + ); + path = Images; + sourceTree = ""; + }; + 43D1CF7F2D808A5000AC1ED9 /* Network */ = { + isa = PBXGroup; + children = ( + 4395E65D2DB00E4100637803 /* BackgroundDownload */, + 43D1CF802D808A5000AC1ED9 /* ImageLoader.swift */, + 43D1CF812D808A5000AC1ED9 /* NetworkService.swift */, + ); + path = Network; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -143,7 +238,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1520; - LastUpgradeCheck = 1520; + LastUpgradeCheck = 1600; TargetAttributes = { 43D1CF4E2D80860C00AC1ED9 = { CreatedOnToolsVersion = 15.2; @@ -155,7 +250,7 @@ }; }; buildConfigurationList = 43D1CF4A2D80860C00AC1ED9 /* Build configuration list for PBXProject "BackgroundTransferRevised-Example" */; - compatibilityVersion = "Xcode 14.0"; + compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -179,7 +274,7 @@ buildActionMask = 2147483647; files = ( 43D1CF5A2D80860E00AC1ED9 /* Preview Assets.xcassets in Resources */, - 43D1CF572D80860E00AC1ED9 /* Assets.xcassets in Resources */, + 43D1CF832D808A5000AC1ED9 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -197,8 +292,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 43D1CF552D80860C00AC1ED9 /* ContentView.swift in Sources */, - 43D1CF532D80860C00AC1ED9 /* BackgroundTransferRevised_ExampleApp.swift in Sources */, + 437CD5112D94C62800A909A6 /* AppDelegate.swift in Sources */, + 436CC0972D808CC500F9E4E2 /* CatsViewModel.swift in Sources */, + 43D1CF852D808A5000AC1ED9 /* NetworkService.swift in Sources */, + 436CC0982D808CC500F9E4E2 /* CatsView.swift in Sources */, + 436CC09B2D8091D300F9E4E2 /* GridItem+Layout.swift in Sources */, + 437CD5102D94C62800A909A6 /* BackgroundTransferRevised_ExampleApp.swift in Sources */, + 4395E65E2DB00E4100637803 /* BackgroundDownloadMetaStore.swift in Sources */, + 4395E65F2DB00E4100637803 /* BackgroundDownloadService.swift in Sources */, + 4395E6632DB00E8300637803 /* BackgroundDownloadTaskStore.swift in Sources */, + 4395E6652DB00ECD00637803 /* BackgroundDownloadDelegator.swift in Sources */, + 43D1CF842D808A5000AC1ED9 /* ImageLoader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,6 +330,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -273,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -281,6 +386,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -289,6 +396,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -330,12 +438,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -348,8 +458,10 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BackgroundTransferRevised-Example/Preview Content\""; + DEVELOPMENT_TEAM = A8RQWWHSHX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "BackgroundTransferRevised-Example/Application/Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -362,9 +474,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransferRevised-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -376,8 +491,10 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BackgroundTransferRevised-Example/Preview Content\""; + DEVELOPMENT_TEAM = A8RQWWHSHX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "BackgroundTransferRevised-Example/Application/Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -390,27 +507,33 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransferRevised-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; 43D1CF772D80860E00AC1ED9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "BackgroundTransferRevised-ExampleTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransferRevised-ExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BackgroundTransferRevised-Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BackgroundTransferRevised-Example"; }; name = Debug; @@ -418,18 +541,21 @@ 43D1CF782D80860E00AC1ED9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "BackgroundTransferRevised-ExampleTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.williamboles.BackgroundTransferRevised-ExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BackgroundTransferRevised-Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BackgroundTransferRevised-Example"; }; name = Release; diff --git a/BackgroundTransferRevised-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransferRevised-Example.xcscheme b/BackgroundTransferRevised-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransferRevised-Example.xcscheme new file mode 100644 index 0000000..2eee759 --- /dev/null +++ b/BackgroundTransferRevised-Example.xcodeproj/xcshareddata/xcschemes/BackgroundTransferRevised-Example.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BackgroundTransferRevised-Example/Application/AppDelegate.swift b/BackgroundTransferRevised-Example/Application/AppDelegate.swift new file mode 100644 index 0000000..59958e6 --- /dev/null +++ b/BackgroundTransferRevised-Example/Application/AppDelegate.swift @@ -0,0 +1,32 @@ +// +// AppDelegate.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 26/03/2025. +// + +import UIKit +import OSLog + +class AppDelegate: NSObject, UIApplicationDelegate { + static var shared: AppDelegate? + + private var backgroundCompletionHandler: (() -> Void)? + private let logger = Logger(subsystem: "com.williamboles", + category: "appDelegate") + + // MARK: - Background + + func application(_ application: UIApplication, + handleEventsForBackgroundURLSession identifier: String, + completionHandler: @escaping () -> Void) { + self.backgroundCompletionHandler = completionHandler + } + + func backgroundDownloadsComplete() { + logger.info("Triggering background session completion handler") + + backgroundCompletionHandler?() + backgroundCompletionHandler = nil + } +} diff --git a/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift new file mode 100644 index 0000000..61b5077 --- /dev/null +++ b/BackgroundTransferRevised-Example/Application/BackgroundTransferRevised_ExampleApp.swift @@ -0,0 +1,48 @@ +// +// BackgroundTransferRevised_ExampleApp.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 11/03/2025. +// + +import SwiftUI +import OSLog + +@main +struct BackgroundTransferRevised_ExampleApp: App { + @Environment(\.scenePhase) private var scenePhase + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + private let logger = Logger(subsystem: "com.williamboles", + category: "app") + + + // MARK: - Init + + init() { + AppDelegate.shared = appDelegate + } + + // MARK: - Scene + + var body: some Scene { + WindowGroup { + let catsViewModel = CatsViewModel() + CatsView(viewModel: catsViewModel) + } + .onChange(of: scenePhase) { (_, newPhase) in + guard newPhase == .background else { + return + } + + logger.info("Files will be downloaded to: \(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteString)") + +// //Exit app to test restoring app from a terminated state. Comment out to test restoring app from a suspended state. +// Task { +// logger.info("Simulating app termination by exit(0)") +// +// exit(0) +// } + } + } +} diff --git a/BackgroundTransferRevised-Example/Application/Info.plist b/BackgroundTransferRevised-Example/Application/Info.plist new file mode 100644 index 0000000..1a1bf62 --- /dev/null +++ b/BackgroundTransferRevised-Example/Application/Info.plist @@ -0,0 +1,11 @@ + + + + + UIBackgroundModes + + fetch + processing + + + diff --git a/BackgroundTransferRevised-Example/Assets.xcassets/AccentColor.colorset/Contents.json b/BackgroundTransferRevised-Example/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/BackgroundTransferRevised-Example/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/BackgroundTransferRevised-Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/BackgroundTransferRevised-Example/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3..0000000 --- a/BackgroundTransferRevised-Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/BackgroundTransferRevised-Example/BackgroundTransferRevised_ExampleApp.swift b/BackgroundTransferRevised-Example/BackgroundTransferRevised_ExampleApp.swift deleted file mode 100644 index 42170d8..0000000 --- a/BackgroundTransferRevised-Example/BackgroundTransferRevised_ExampleApp.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// BackgroundTransferRevised_ExampleApp.swift -// BackgroundTransferRevised-Example -// -// Created by William Boles on 11/03/2025. -// - -import SwiftUI - -@main -struct BackgroundTransferRevised_ExampleApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/BackgroundTransferRevised-Example/ContentView.swift b/BackgroundTransferRevised-Example/ContentView.swift deleted file mode 100644 index 080de83..0000000 --- a/BackgroundTransferRevised-Example/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// BackgroundTransferRevised-Example -// -// Created by William Boles on 11/03/2025. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/BackgroundTransferRevised-Example/Extensions/GridItem+Layout.swift b/BackgroundTransferRevised-Example/Extensions/GridItem+Layout.swift new file mode 100644 index 0000000..3b8aaa1 --- /dev/null +++ b/BackgroundTransferRevised-Example/Extensions/GridItem+Layout.swift @@ -0,0 +1,22 @@ +// +// GridItem+Layout.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 11/03/2025. +// + +import Foundation +import SwiftUI + +extension GridItem { + + static func threeFlexibleColumns() -> [GridItem] { + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + ] + + return columns + } +} diff --git a/BackgroundTransferRevised-Example/Features/Cats/CatsView.swift b/BackgroundTransferRevised-Example/Features/Cats/CatsView.swift new file mode 100644 index 0000000..e2ca05b --- /dev/null +++ b/BackgroundTransferRevised-Example/Features/Cats/CatsView.swift @@ -0,0 +1,70 @@ +// +// ContentView.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 11/03/2025. +// + +import SwiftUI + +struct CatsView: View { + @StateObject var viewModel: CatsViewModel + + // MARK: - View + + var body: some View { + NavigationStack { + VStack { + switch viewModel.state { + case .empty: + Text("We have no cats to show you! 🙀") + case .retrieving: + ProgressView("Retrieving Cats! 😺") + case .retrieved(let cats): + GeometryReader { geometryReader in + let columns = GridItem.threeFlexibleColumns() + let sideLength = geometryReader.size.width / CGFloat(columns.count) + ScrollView { + LazyVGrid(columns: columns, alignment: .center, spacing: 4) { + ForEach(cats) { catViewModel in + CatImageCell(viewModel: catViewModel) + .frame(width: sideLength, height: sideLength) + .task { + await catViewModel.loadImage() + } + } + } + } + } + case .failed: + Text("Failed to retrieve Cats! 😿") + } + } + .padding() + .navigationTitle("Cats 😻") + } + .task { + await viewModel.retrieveCats() + } + } +} + +struct CatImageCell: View { + @StateObject var viewModel: CatViewModel + + // MARK: - View + + var body: some View { + switch viewModel.state { + case .empty: + Image(systemName: "photo") + case .retrieving: + ProgressView() + case .retrieved(let image): + image.resizable() + .aspectRatio(contentMode: .fill) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .clipped() + } + } +} diff --git a/BackgroundTransferRevised-Example/Features/Cats/CatsViewModel.swift b/BackgroundTransferRevised-Example/Features/Cats/CatsViewModel.swift new file mode 100644 index 0000000..c1bf88e --- /dev/null +++ b/BackgroundTransferRevised-Example/Features/Cats/CatsViewModel.swift @@ -0,0 +1,78 @@ +// +// ViewModelProvider.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 31/01/2023. +// + +import Foundation +import SwiftUI +import UIKit + +@MainActor +class CatsViewModel: ObservableObject { + enum CatsState { + case empty + case retrieving + case retrieved(_ cats: [CatViewModel]) + case failed + } + + @Published var state: CatsState = .empty + + private let networkService = NetworkService() + + // MARK: - Retrieval + + func retrieveCats() async { + state = .retrieving + + do { + let cats = try await networkService.retrieveCats() + + let viewModels = cats.map { CatViewModel(cat: $0) } + state = .retrieved(viewModels) + } catch { + state = .failed + } + } +} + +@MainActor +class CatViewModel: ObservableObject, Identifiable { + enum CatState { + case empty + case retrieving + case retrieved(_ image: Image) + } + + @Published var state: CatState = .empty + + private let imageLoader = ImageLoader() + private let cat: Cat + + let id: String + + // MARK: - Init + + init(cat: Cat) { + self.cat = cat + self.id = cat.id + } + + // MARK: - Image + + func loadImage() async { + state = .retrieving + + let uiImage = try? await imageLoader.loadImage(name: cat.id, + url: cat.url) + + guard let uiImage else { + state = .empty + return + } + + state = .retrieved(Image(uiImage: uiImage)) + } +} diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift new file mode 100644 index 0000000..45953a4 --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadDelegator.swift @@ -0,0 +1,138 @@ +// +// BackgroundDownloadDelegator.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 16/04/2025. +// + +import Foundation +import OSLog + +final class BackgroundDownloadDelegator: NSObject, URLSessionDownloadDelegate { + private let metaStore: BackgroundDownloadMetaStore + private let taskStore: BackgroundDownloadTaskStore + private let logger: Logger + + // MARK: - Init + + init(metaStore: BackgroundDownloadMetaStore, + logger: Logger) { + self.metaStore = metaStore + self.logger = logger + self.taskStore = BackgroundDownloadTaskStore() + } + + // MARK: - URLSessionDownloadDelegate + + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { + guard let fromURL = downloadTask.originalRequest?.url else { + logger.error("Unexpected nil URL for download task.") + return + } + + logger.info("Download request completed for: \(fromURL.absoluteString)") + + let tempLocation = FileManager.default.temporaryDirectory.appendingPathComponent(location.lastPathComponent) + try? FileManager.default.moveItem(at: location, to: tempLocation) + + let processingTask = Task { + defer { + cleanUpDownload(forURL: fromURL) + } + + let metaData = await metaStore.retrieveMetadata(key: fromURL.absoluteString) + guard let metaData else { + logger.error("Unable to find existing download item for: \(fromURL.absoluteString)") + return + } + + guard let response = downloadTask.response as? HTTPURLResponse, response.statusCode == 200 else { + logger.error("Unexpected response for: \(fromURL.absoluteString)") + metaData.continuation?.resume(throwing: BackgroundDownloadError.serverError(downloadTask.response)) + return + } + + logger.info("Download successful for: \(fromURL.absoluteString)") + + do { + try FileManager.default.moveItem(at: tempLocation, + to: metaData.toURL) + metaData.continuation?.resume(returning: metaData.toURL) + } catch { + logger.error("File system error while moving file: \(error.localizedDescription)") + metaData.continuation?.resume(throwing: BackgroundDownloadError.fileSystemError(error)) + } + } + + // TODO: Update processing to use a serial queue + Task { + await taskStore.storeTask(processingTask, + key: fromURL.absoluteString) + } + } + + func urlSession(_ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error?) { + guard let error = error, + let fromURL = task.originalRequest?.url else { + return + } + + logger.info("Download failed for: \(fromURL.absoluteString), error: \(error.localizedDescription)") + + let processingTask = Task { + defer { + cleanUpDownload(forURL: fromURL) + } + + let metaData = await metaStore.retrieveMetadata(key: fromURL.absoluteString) + metaData?.continuation?.resume(throwing: BackgroundDownloadError.clientError(error)) + } + + // TODO: Update processing to use a serial queue + Task { + await taskStore.storeTask(processingTask, + key: fromURL.absoluteString) + } + } + + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + logger.info("Did finish events for background session") + + Task { + await withTaskGroup(of: Void.self) { group in + for task in await taskStore.retrieveAll() { + group.addTask { + await task.value + } + } + + await group.waitForAll() + + logger.info("All tasks in group completed") + + await MainActor.run { + guard let appDelegate = AppDelegate.shared else { + logger.error("App delegate is nil") + return + } + + appDelegate.backgroundDownloadsComplete() + } + } + } + } + + private func cleanUpDownload(forURL url: URL) { + Task { + let key = url.absoluteString + + await metaStore.removeMetadata(key: key) + await taskStore.removeTask(key: key) + } + } +} + diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift new file mode 100644 index 0000000..2888e2a --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadMetaStore.swift @@ -0,0 +1,56 @@ +// +// BackgroundDownloadStore.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 26/03/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation + +struct BackgroundDownloadMetaData { + let toURL: URL + let continuation: CheckedContinuation? +} + +actor BackgroundDownloadMetaStore { + private var inMemoryStore: [String: CheckedContinuation] + private let persistentStore: UserDefaults + + // MARK: - Init + + init() { + self.inMemoryStore = [String: CheckedContinuation]() + self.persistentStore = UserDefaults.standard + } + + // MARK: - Store + + func storeMetadata(_ metaData: BackgroundDownloadMetaData, + key: String) { + inMemoryStore[key] = metaData.continuation + persistentStore.set(metaData.toURL, forKey: key) + } + + // MARK: - Retrieve + + func retrieveMetadata(key: String) -> BackgroundDownloadMetaData? { + guard let toURL = persistentStore.url(forKey: key) else { + return nil + } + + let continuation = inMemoryStore[key] + + let metaData = BackgroundDownloadMetaData(toURL: toURL, + continuation: continuation) + + return metaData + } + + // MARK: - Remove + + func removeMetadata(key: String) { + inMemoryStore.removeValue(forKey: key) + persistentStore.removeObject(forKey: key) + } +} diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift new file mode 100644 index 0000000..6bbcf24 --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadService.swift @@ -0,0 +1,74 @@ +// +// BackgroundDownloadService.swift +// BackgroundTransfer-Example +// +// Created by William Boles on 26/03/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import OSLog +import UIKit +import SwiftUI + +enum BackgroundDownloadError: Error { + case fileSystemError(_ underlyingError: Error) + case clientError(_ underlyingError: Error) + case serverError(_ underlyingResponse: URLResponse?) +} + +actor BackgroundDownloadService { + private let session: URLSession + private let metaStore: BackgroundDownloadMetaStore + private let logger: Logger + + // MARK: - Singleton + + static let shared = BackgroundDownloadService() + + // MARK: - Init + + private init() { + self.metaStore = BackgroundDownloadMetaStore() + self.logger = Logger(subsystem: "com.williamboles", + category: "background.download") + + let delegator = BackgroundDownloadDelegator(metaStore: metaStore, + logger: logger) + + let configuration = URLSessionConfiguration.background(withIdentifier: "com.williamboles.background.download.session") + configuration.isDiscretionary = false + configuration.sessionSendsLaunchEvents = true + self.session = URLSession(configuration: configuration, + delegate: delegator, + delegateQueue: nil) + } + + // MARK: - Download + + func download(from fromURL: URL, + to toURL: URL) async throws -> URL { + return try await withCheckedThrowingContinuation { continuation in + logger.info("Scheduling download: \(fromURL.absoluteString)") + + storeMetadata(from: fromURL, + to: toURL, + continuation: continuation) + + let downloadTask = session.downloadTask(with: fromURL) + downloadTask.earliestBeginDate = Date().addingTimeInterval(10) // Remove this in production, the delay was added for demonstration purposes only + downloadTask.resume() + } + } + + private func storeMetadata(from fromURL: URL, + to toURL: URL, + continuation: CheckedContinuation) { + Task { + let metaData = BackgroundDownloadMetaData(toURL: toURL, + continuation: continuation) + await metaStore.storeMetadata(metaData, + key: fromURL.absoluteString) + } + } +} diff --git a/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift new file mode 100644 index 0000000..59400fe --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/BackgroundDownload/BackgroundDownloadTaskStore.swift @@ -0,0 +1,31 @@ +// +// BackgroundDownloadTaskStore.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 16/04/2025. +// + +import Foundation + +actor BackgroundDownloadTaskStore { + private var tasks = [String: Task]() + + // MARK: - Add + + func storeTask(_ task: Task, + key: String) { + tasks[key] = task + } + + // MARK: - Retrieve + + func retrieveAll() -> [Task] { + Array(tasks.values) + } + + // MARK: - Remove + + func removeTask(key: String) { + tasks[key] = nil + } +} diff --git a/BackgroundTransferRevised-Example/Network/ImageLoader.swift b/BackgroundTransferRevised-Example/Network/ImageLoader.swift new file mode 100644 index 0000000..880b130 --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/ImageLoader.swift @@ -0,0 +1,68 @@ +// +// ImageLoader.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 11/03/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import UIKit + +enum ImageLoaderError: Error { + case missingData + case invalidImageData +} + +actor ImageLoader { + private let backgroundDownloader: BackgroundDownloadService + + // MARK: - Init + + init() { + self.backgroundDownloader = BackgroundDownloadService.shared + } + + // MARK: - Load + + func loadImage(name: String, + url: URL) async throws -> UIImage { + 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) { + let image = try await loadLocalImage(localImageURL: localImageURL) + + return image + } else { + let image = try await loadRemoteImage(remoteImageURL: url, + localImageURL: localImageURL) + + return image + } + } + + + private func loadLocalImage(localImageURL: URL) async throws -> UIImage { + guard let imageData = try? Data(contentsOf: localImageURL) else { + throw ImageLoaderError.missingData + } + + guard let image = UIImage(data: imageData) else { + throw ImageLoaderError.invalidImageData + } + + return image + } + + private func loadRemoteImage(remoteImageURL: URL, + localImageURL: URL) async throws -> UIImage { + let url = try await backgroundDownloader.download(from: remoteImageURL, + to: localImageURL) + let image = try await loadLocalImage(localImageURL: url) + + return image + } +} diff --git a/BackgroundTransferRevised-Example/Network/NetworkService.swift b/BackgroundTransferRevised-Example/Network/NetworkService.swift new file mode 100644 index 0000000..5bd8807 --- /dev/null +++ b/BackgroundTransferRevised-Example/Network/NetworkService.swift @@ -0,0 +1,80 @@ +// +// NetworkService.swift +// BackgroundTransferRevised-Example +// +// Created by William Boles on 11/03/2025. +// Copyright © 2025 William Boles. All rights reserved. +// + +import Foundation +import OSLog + +struct Cat: Decodable, Equatable { + let id: String + let url: URL +} + +enum NetworkServiceError: Error { + case networkError + case decodingErrror +} + +actor NetworkService { + private let logger: Logger + + // MARK: - Init + + init() { + self.logger = Logger(subsystem: "com.williamboles", + category: "NetworkService") + } + + // MARK: - Cats + + func retrieveCats() async throws -> [Cat] { + 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 + + guard let url = components.url else { + throw NetworkServiceError.networkError + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "GET" + urlRequest.addValue(APIKey, forHTTPHeaderField: "x-api-key") + + logger.info("Retrieving cats...") + + do { + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw NetworkServiceError.networkError + } + + guard let cats = try? JSONDecoder().decode([Cat].self, from: data) else { + throw NetworkServiceError.decodingErrror + } + + logger.info("Cats successfully retrieved!") + + return cats + } catch let error as NetworkServiceError { + throw error + } catch { + throw NetworkServiceError.networkError + } + } +} diff --git a/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AccentColor.colorset/Contents.json b/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..22c4bb0 --- /dev/null +++ b/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AppIcon.appiconset/Contents.json b/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BackgroundTransferRevised-Example/Assets.xcassets/Contents.json b/BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/Contents.json similarity index 100% rename from BackgroundTransferRevised-Example/Assets.xcassets/Contents.json rename to BackgroundTransferRevised-Example/Resources/Images/Assets.xcassets/Contents.json diff --git a/BackgroundTransferRevised-ExampleTests/BackgroundTransferRevised_ExampleTests.swift b/BackgroundTransferRevised-ExampleTests/BackgroundTransferRevised_ExampleTests.swift index 45e8427..a8b9eb4 100644 --- a/BackgroundTransferRevised-ExampleTests/BackgroundTransferRevised_ExampleTests.swift +++ b/BackgroundTransferRevised-ExampleTests/BackgroundTransferRevised_ExampleTests.swift @@ -25,12 +25,4 @@ final class BackgroundTransferRevised_ExampleTests: XCTestCase { // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/BackgroundTransferRevised-ExampleTests/Info.plist b/BackgroundTransferRevised-ExampleTests/Info.plist new file mode 100644 index 0000000..1a1bf62 --- /dev/null +++ b/BackgroundTransferRevised-ExampleTests/Info.plist @@ -0,0 +1,11 @@ + + + + + UIBackgroundModes + + fetch + processing + + + diff --git a/Gemfile b/Gemfile index 628d7a1..6f0518a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane", "2.226.0" +gem "fastlane", "2.227.0" diff --git a/Gemfile.lock b/Gemfile.lock index 210bf3b..cf51386 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,13 +10,14 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.2) - aws-partitions (1.1064.0) - aws-sdk-core (3.220.1) + aws-partitions (1.1075.0) + aws-sdk-core (3.221.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) + logger aws-sdk-kms (1.99.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) @@ -69,7 +70,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.226.0) + fastlane (2.227.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -156,9 +157,10 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.10.1) + json (2.10.2) jwt (2.10.1) base64 + logger (1.6.6) mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) @@ -209,7 +211,7 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.4.0) + xcpretty (0.4.1) rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) @@ -219,7 +221,7 @@ PLATFORMS x86_64-darwin-22 DEPENDENCIES - fastlane (= 2.226.0) + fastlane (= 2.227.0) BUNDLED WITH 2.6.5