├── ProductClean ├── ProductClean │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── Info.plist │ ├── Modules │ │ └── Products │ │ │ ├── Data │ │ │ ├── Services │ │ │ │ ├── APIEndpoint.swift │ │ │ │ └── ProductListService.swift │ │ │ ├── Repository │ │ │ │ └── ProductListRepository.swift │ │ │ └── Entity │ │ │ │ └── ProductDataListDTO.swift │ │ │ ├── Domain │ │ │ ├── Interface │ │ │ │ └── Repository │ │ │ │ │ └── ProductListRepositoryProtocol.swift │ │ │ ├── Entity │ │ │ │ └── ProductDomainListDTO.swift │ │ │ └── UseCase │ │ │ │ └── ProductListUseCase.swift │ │ │ ├── Presentation │ │ │ ├── ViewModel │ │ │ │ ├── ProductListItemViewModel.swift │ │ │ │ └── ProductListViewModel.swift │ │ │ ├── Views │ │ │ │ ├── ProductItemView.swift │ │ │ │ ├── HeaderImageView.swift │ │ │ │ ├── ProductListLayout.swift │ │ │ │ ├── ProductDetailsView.swift │ │ │ │ ├── ErrorView.swift │ │ │ │ ├── ProductListView_Previews.swift │ │ │ │ └── ProductListView.swift │ │ │ └── Util │ │ │ │ ├── ImageDownloadAuthChallenge │ │ │ │ └── ImageDownloadAuthenticationChallenge.swift │ │ │ │ └── Extensions │ │ │ │ ├── Double+CurrencyAmount.swift │ │ │ │ └── URLAuthenticationChallenge+TrustServer.swift │ │ │ └── ProductsModule.swift │ ├── Application │ │ ├── AppConfiguration.swift │ │ ├── ProductCleanApp.swift │ │ └── AppDIContainer.swift │ ├── Common │ │ └── Constants.swift │ └── Networking │ │ ├── SharedURLSession.swift │ │ ├── URLSessionProtocol.swift │ │ ├── NetworkConfig.swift │ │ ├── SharedURLSessionDelegate.swift │ │ ├── NetworkSessionManager.swift │ │ ├── NetworkError.swift │ │ ├── NetworkRequest.swift │ │ ├── DataTransferService.swift │ │ ├── NetworkManager.swift │ │ └── URLRequestCreater.swift ├── Pods │ ├── Target Support Files │ │ ├── Pods-ProductClean │ │ │ ├── Pods-ProductClean-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-ProductClean-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-ProductClean.modulemap │ │ │ ├── Pods-ProductClean-dummy.m │ │ │ ├── Pods-ProductClean-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-ProductClean-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-ProductClean-umbrella.h │ │ │ ├── Pods-ProductClean-Info.plist │ │ │ ├── Pods-ProductClean.debug.xcconfig │ │ │ ├── Pods-ProductClean.release.xcconfig │ │ │ ├── Pods-ProductClean-acknowledgements.markdown │ │ │ └── Pods-ProductClean-acknowledgements.plist │ │ ├── Kingfisher │ │ │ ├── Kingfisher.modulemap │ │ │ ├── Kingfisher-dummy.m │ │ │ ├── Kingfisher-prefix.pch │ │ │ ├── Kingfisher-umbrella.h │ │ │ ├── ResourceBundle-Kingfisher-Kingfisher-Info.plist │ │ │ ├── Kingfisher-Info.plist │ │ │ ├── Kingfisher.debug.xcconfig │ │ │ └── Kingfisher.release.xcconfig │ │ ├── Pods-ProductCleanSnapshotTests │ │ │ ├── Pods-ProductCleanSnapshotTests-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-ProductCleanSnapshotTests-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-ProductCleanSnapshotTests.modulemap │ │ │ ├── Pods-ProductCleanSnapshotTests-dummy.m │ │ │ ├── Pods-ProductCleanSnapshotTests-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-ProductCleanSnapshotTests-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-ProductCleanSnapshotTests-umbrella.h │ │ │ ├── Pods-ProductCleanSnapshotTests-Info.plist │ │ │ ├── Pods-ProductCleanSnapshotTests-acknowledgements.markdown │ │ │ ├── Pods-ProductCleanSnapshotTests.debug.xcconfig │ │ │ ├── Pods-ProductCleanSnapshotTests.release.xcconfig │ │ │ └── Pods-ProductCleanSnapshotTests-acknowledgements.plist │ │ ├── Pods-ProductClean-ProductCleanUITests │ │ │ ├── Pods-ProductClean-ProductCleanUITests-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-ProductClean-ProductCleanUITests-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-ProductClean-ProductCleanUITests.modulemap │ │ │ ├── Pods-ProductClean-ProductCleanUITests-dummy.m │ │ │ ├── Pods-ProductClean-ProductCleanUITests-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-ProductClean-ProductCleanUITests-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-ProductClean-ProductCleanUITests-umbrella.h │ │ │ ├── Pods-ProductClean-ProductCleanUITests-Info.plist │ │ │ ├── Pods-ProductClean-ProductCleanUITests.debug.xcconfig │ │ │ ├── Pods-ProductClean-ProductCleanUITests.release.xcconfig │ │ │ ├── Pods-ProductClean-ProductCleanUITests-acknowledgements.markdown │ │ │ └── Pods-ProductClean-ProductCleanUITests-acknowledgements.plist │ │ ├── SnapshotTesting │ │ │ ├── SnapshotTesting.modulemap │ │ │ ├── SnapshotTesting-dummy.m │ │ │ ├── SnapshotTesting-prefix.pch │ │ │ ├── SnapshotTesting-umbrella.h │ │ │ ├── SnapshotTesting-Info.plist │ │ │ ├── SnapshotTesting.debug.xcconfig │ │ │ └── SnapshotTesting.release.xcconfig │ │ └── Pods-ProductCleanTests │ │ │ ├── Pods-ProductCleanTests.modulemap │ │ │ ├── Pods-ProductCleanTests-acknowledgements.markdown │ │ │ ├── Pods-ProductCleanTests-dummy.m │ │ │ ├── Pods-ProductCleanTests-umbrella.h │ │ │ ├── Pods-ProductCleanTests.debug.xcconfig │ │ │ ├── Pods-ProductCleanTests.release.xcconfig │ │ │ ├── Pods-ProductCleanTests-acknowledgements.plist │ │ │ └── Pods-ProductCleanTests-Info.plist │ ├── SnapshotTesting │ │ ├── Sources │ │ │ └── SnapshotTesting │ │ │ │ ├── SnapshotTestCase.swift │ │ │ │ ├── Common │ │ │ │ ├── XCTAttachment.swift │ │ │ │ ├── Internal.swift │ │ │ │ └── String+SpecialCharacters.swift │ │ │ │ ├── Snapshotting │ │ │ │ ├── Description.swift │ │ │ │ ├── Data.swift │ │ │ │ ├── String.swift │ │ │ │ ├── NSViewController.swift │ │ │ │ ├── CaseIterable.swift │ │ │ │ ├── SceneKit.swift │ │ │ │ ├── SpriteKit.swift │ │ │ │ ├── Codable.swift │ │ │ │ ├── CALayer.swift │ │ │ │ ├── UIBezierPath.swift │ │ │ │ ├── NSView.swift │ │ │ │ ├── UIView.swift │ │ │ │ ├── SwiftUIView.swift │ │ │ │ ├── URLRequest.swift │ │ │ │ └── NSBezierPath.swift │ │ │ │ ├── Extensions │ │ │ │ └── Wait.swift │ │ │ │ ├── Diffing.swift │ │ │ │ └── Async.swift │ │ └── LICENSE │ ├── Manifest.lock │ └── Kingfisher │ │ ├── Sources │ │ ├── PrivacyInfo.xcprivacy │ │ ├── Utility │ │ │ ├── Box.swift │ │ │ ├── Runtime.swift │ │ │ ├── Result.swift │ │ │ ├── CallbackQueue.swift │ │ │ └── Delegate.swift │ │ ├── Networking │ │ │ ├── ImageDataProcessor.swift │ │ │ └── RedirectHandler.swift │ │ ├── Image │ │ │ ├── GraphicsContext.swift │ │ │ └── Placeholder.swift │ │ ├── SwiftUI │ │ │ ├── ImageContext.swift │ │ │ └── KFImage.swift │ │ ├── General │ │ │ └── Kingfisher.swift │ │ └── Cache │ │ │ └── Storage.swift │ │ └── LICENSE ├── ProductClean.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── ProductCleanSnapshotTests │ ├── __Snapshots__ │ │ └── SnapshotTestVCExtension │ │ │ ├── Snapshot.ErrorView.png │ │ │ ├── Snapshot.HeaderImageView.png │ │ │ ├── Snapshot.ProductDetailsView.png │ │ │ ├── Snapshot.ProductListCellView.png │ │ │ ├── Snapshot.ProductListLayoutView.png │ │ │ ├── Snapshot.ProductListViewFailure.png │ │ │ └── Snapshot.ProductListViewSuccess.png │ ├── View+ToVC.swift │ ├── SnapshotTestVCExtension.swift │ ├── Mock │ │ ├── ProductListViewModelMock.swift │ │ └── MockData.swift │ ├── ErrorViewSnapshotTests.swift │ ├── ProductDetailsViewSnapshotTests.swift │ ├── HeaderImageViewSnapshotTests.swift │ ├── ProductItemViewSnapshotTests.swift │ ├── ProductListLayoutSnapshotTests.swift │ └── ProductListViewSnapshotTests.swift ├── ProductClean.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile.lock ├── ProductCleanTests │ ├── Networking │ │ ├── Mock │ │ │ ├── MockNetworkManager.swift │ │ │ ├── MockURLSession.swift │ │ │ ├── MockNetworkSessionManager.swift │ │ │ └── MockNetworkRequest.swift │ │ ├── URLRequestCreaterTests.swift │ │ ├── NetworkSessionManagerTests.swift │ │ ├── DataTransferTests.swift │ │ └── NetworkManagerTests.swift │ ├── Products │ │ ├── Mock │ │ │ ├── MockProductListUseCase.swift │ │ │ ├── MockProductListRepository.swift │ │ │ ├── MockProductListService.swift │ │ │ ├── MockDataTransferService.swift │ │ │ └── MockData.swift │ │ ├── ProductListRepositoryTest.swift │ │ ├── ProductListUseCaseTests.swift │ │ ├── ProductListServiceTests.swift │ │ └── ProductListViewModelTests.swift │ └── Resources │ │ └── Products.json ├── Podfile └── ProductCleanUITests │ ├── ProductCleanUITestsLaunchTests.swift │ └── ProductCleanUITests.swift ├── .swiftlint.yml ├── README.md └── .gitignore /ProductClean/ProductClean/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap: -------------------------------------------------------------------------------- 1 | framework module Kingfisher { 2 | umbrella header "Kingfisher-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Kingfisher : NSObject 3 | @end 4 | @implementation PodsDummy_Kingfisher 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapshotTesting.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapshotTesting.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting.modulemap: -------------------------------------------------------------------------------- 1 | framework module SnapshotTesting { 2 | umbrella header "SnapshotTesting-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ProductClean { 2 | umbrella header "Pods-ProductClean-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SnapshotTesting : NSObject 3 | @end 4 | @implementation PodsDummy_SnapshotTesting 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ProductClean : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ProductClean 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/ProductClean.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ProductCleanTests { 2 | umbrella header "Pods-ProductCleanTests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/SnapshotTestCase.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @available(swift, obsoleted: 5.0, renamed: "XCTestCase", message: "Please use XCTestCase instead") 4 | public typealias SnapshotTestCase = XCTestCase 5 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductClean/Pods-ProductClean-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ProductCleanTests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ProductCleanTests 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Common/XCTAttachment.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Foundation 3 | 4 | public struct XCTAttachment { 5 | public init(data: Data) {} 6 | public init(data: Data, uniformTypeIdentifier: String) {} 7 | } 8 | #endif 9 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ErrorView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ErrorView.png -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ProductCleanSnapshotTests { 2 | umbrella header "Pods-ProductCleanSnapshotTests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.HeaderImageView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.HeaderImageView.png -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ProductCleanSnapshotTests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ProductCleanSnapshotTests 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Data/Services/APIEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIEndpoint.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 25/02/24. 6 | // 7 | 8 | import Foundation 9 | struct APIEndpoint { 10 | static let products = "/products" 11 | } 12 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductDetailsView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductDetailsView.png -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListCellView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListCellView.png -------------------------------------------------------------------------------- /ProductClean/ProductClean/Application/AppConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfiguration.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AppConfiguration { 11 | static let baseURL = "dummyjson.com" 12 | } 13 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListLayoutView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListLayoutView.png -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListViewFailure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListViewFailure.png -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListViewSuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sajib-ghosh-iOS/Product_Clean_Architecture/HEAD/ProductClean/ProductCleanSnapshotTests/__Snapshots__/SnapshotTestVCExtension/Snapshot.ProductListViewSuccess.png -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ProductClean_ProductCleanUITests { 2 | umbrella header "Pods-ProductClean-ProductCleanUITests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ProductClean_ProductCleanUITests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ProductClean_ProductCleanUITests 5 | @end 6 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /ProductClean/ProductClean.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ProductClean/ProductClean.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ProductClean/ProductClean.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Common/Internal.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Cocoa 3 | typealias Image = NSImage 4 | typealias ImageView = NSImageView 5 | typealias View = NSView 6 | #elseif os(iOS) || os(tvOS) 7 | import UIKit 8 | typealias Image = UIImage 9 | typealias ImageView = UIImageView 10 | typealias View = UIView 11 | #endif 12 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Domain/Interface/Repository/ProductListRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListRepositoryProtocol.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductListRepository { 11 | func fetchProductList() async throws -> [ProductDomainListDTO] 12 | } 13 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/Description.swift: -------------------------------------------------------------------------------- 1 | extension Snapshotting where Format == String { 2 | /// A snapshot strategy that captures a value's textual description from `String`'s `init(description:)` 3 | /// initializer. 4 | public static var description: Snapshotting { 5 | return SimplySnapshotting.lines.pullback(String.init(describing:)) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double KingfisherVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Domain/Entity/ProductDomainListDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDomainListDTO.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ProductDomainListDTO { 11 | let productId: Int 12 | let title: String 13 | let description: String 14 | let price: Double 15 | var category: String 16 | let thumbnail: String 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Common/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConstant.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AppConstant { 11 | static let errorTitle = "Error" 12 | static let productListTitle = "Products" 13 | static let retry = "Retry" 14 | static let errorImage = "exclamationmark.triangle.fill" 15 | static let currencyIndentifier = "en_US" 16 | } 17 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/ViewModel/ProductListItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductItemViewModel.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ProductListItemViewModel: Hashable { 11 | var id: Int 12 | var title: String 13 | var description: String 14 | var price: String 15 | var category: String 16 | var image: String 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SnapshotTestingVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SnapshotTestingVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ProductCleanVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ProductCleanVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/View+ToVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView+ToVC.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 28/02/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | extension View { 11 | func toViewController() -> UIViewController { 12 | let viewController = UIHostingController(rootView: self) 13 | viewController.view.frame = UIScreen.main.bounds 14 | return viewController 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ProductCleanTestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ProductCleanTestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Kingfisher (7.11.0) 3 | - SnapshotTesting (1.9.0) 4 | 5 | DEPENDENCIES: 6 | - Kingfisher (~> 7.0) 7 | - SnapshotTesting 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - Kingfisher 12 | - SnapshotTesting 13 | 14 | SPEC CHECKSUMS: 15 | Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac 16 | SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b 17 | 18 | PODFILE CHECKSUM: 1c570fb1f37aed8455926d9c6563e430e68a8667 19 | 20 | COCOAPODS: 1.12.1 21 | -------------------------------------------------------------------------------- /ProductClean/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Kingfisher (7.11.0) 3 | - SnapshotTesting (1.9.0) 4 | 5 | DEPENDENCIES: 6 | - Kingfisher (~> 7.0) 7 | - SnapshotTesting 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - Kingfisher 12 | - SnapshotTesting 13 | 14 | SPEC CHECKSUMS: 15 | Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac 16 | SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b 17 | 18 | PODFILE CHECKSUM: 1c570fb1f37aed8455926d9c6563e430e68a8667 19 | 20 | COCOAPODS: 1.12.1 21 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ProductCleanSnapshotTestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ProductCleanSnapshotTestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/SharedURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedURLSession.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class SharedURLSession { 11 | 12 | static var shared: URLSession { 13 | let configuration = URLSessionConfiguration.default 14 | let delegate = SharedURLSessionDelegate() 15 | return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/Mock/MockNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNetworkManager.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | import Foundation 8 | @testable import ProductClean 9 | 10 | class MockNetworkManager: NetworkManager { 11 | var data: Data? 12 | var error: Error? 13 | func fetch(request: NetworkRequest) async throws -> Data { 14 | if let error { 15 | throw error 16 | } 17 | return data! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/URLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionProtocol.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 28/02/24. 6 | // 7 | 8 | import Foundation 9 | protocol URLSessionProtocol { 10 | func asyncData(for request: URLRequest) async throws -> (Data?, URLResponse?) 11 | } 12 | extension URLSession: URLSessionProtocol { 13 | 14 | func asyncData(for request: URLRequest) async throws -> (Data?, URLResponse?) { 15 | return try await self.data(for: request) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ProductClean_ProductCleanUITestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ProductClean_ProductCleanUITestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/Mock/MockProductListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockProductListUseCase.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | @testable import ProductClean 9 | 10 | class MockProductListUseCase: ProductListUseCase { 11 | 12 | var response: [ProductDomainListDTO]? 13 | var error: Error? 14 | 15 | func fetchProductList() async throws -> [ProductDomainListDTO] { 16 | if let error { 17 | throw error 18 | } 19 | return response! 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/Mock/MockProductListRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockProductListRepository.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | @testable import ProductClean 9 | 10 | final class MockProductListRepository: ProductListRepository { 11 | 12 | var response: [ProductDomainListDTO]? 13 | var error: Error? 14 | 15 | func fetchProductList() async throws -> [ProductDomainListDTO] { 16 | if let error { 17 | throw error 18 | } 19 | return response! 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/Mock/MockProductListService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockProductListService.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | @testable import ProductClean 9 | 10 | final class MockProductListService: ProductListService { 11 | 12 | var response: ProductPageDataListDTO? 13 | var error: Error? 14 | 15 | func fetchProductListFromNetwork() async throws -> ProductPageDataListDTO { 16 | if let error { 17 | throw error 18 | } 19 | return response! 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | extension Snapshotting where Value == Data, Format == Data { 5 | public static var data: Snapshotting { 6 | return .init( 7 | pathExtension: nil, 8 | diffing: .init(toData: { $0 }, fromData: { $0 }) { old, new in 9 | guard old != new else { return nil } 10 | let message = old.count == new.count 11 | ? "Expected data to match" 12 | : "Expected \(new) to match \(old)" 13 | return (message, []) 14 | } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/Mock/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSession.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 27/02/24. 6 | // 7 | 8 | import Foundation 9 | @testable import ProductClean 10 | 11 | class MockURLSession: URLSessionProtocol { 12 | var data: Data? 13 | var urlResponse: URLResponse? 14 | var error: Error? 15 | func asyncData(for request: URLRequest) async throws -> (Data?, URLResponse?) { 16 | if let error { 17 | throw error 18 | } 19 | return (data, urlResponse) 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/SnapshotTestVCExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotTestVCExtension.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 05/03/24. 6 | // 7 | 8 | import Foundation 9 | import SnapshotTesting 10 | 11 | extension UIViewController { 12 | func performSnapshotTests( 13 | named name: String, 14 | testName: String = "Snapshot") { 15 | assertSnapshot( 16 | matching: self, 17 | as: .image(precision: 0.9), 18 | named: name, 19 | testName: testName) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/Mock/MockDataTransferService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDataTransferService.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 25/02/24. 6 | // 7 | 8 | import Foundation 9 | @testable import ProductClean 10 | 11 | final class MockDataTransferService: DataTransferService { 12 | 13 | var response: Decodable? 14 | var error: Error? 15 | 16 | func request(request: ProductClean.NetworkRequest) async throws -> T where T : Decodable { 17 | if let error { 18 | throw error 19 | } 20 | return response as! T 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/NetworkConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConfigurable.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 24/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NetworkConfigurable { 11 | var baseURL: String { get } 12 | var headers: [String: String] { get } 13 | } 14 | 15 | class ApiDataNetworkConfig: NetworkConfigurable { 16 | 17 | let baseURL: String 18 | let headers: [String: String] 19 | 20 | init(baseURL: String, 21 | headers: [String: String] = [:]) { 22 | self.baseURL = baseURL 23 | self.headers = headers 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Data/Repository/ProductListRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListRepository.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultProductListRepository: ProductListRepository { 11 | 12 | private let service: ProductListService 13 | 14 | init(service: ProductListService) { 15 | self.service = service 16 | } 17 | 18 | func fetchProductList() async throws -> [ProductDomainListDTO] { 19 | try await service.fetchProductListFromNetwork().products.map { $0.toDomain() } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/Mock/ProductListViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListViewModelMock.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | @testable import ProductClean 9 | 10 | class ProductListViewModelMock: ProductListViewModelProtocol { 11 | 12 | var products: [ProductListItemViewModel] = [] 13 | var isEmpty: Bool { return products.isEmpty } 14 | var isError: Bool = false 15 | var error: String = "Error" 16 | var title: String = "Products" 17 | func fetchProducts() async {} 18 | func shouldShowLoader() -> Bool { isEmpty && !isError } 19 | } 20 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/SharedURLSessionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedURLSessionDelegate.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class SharedURLSessionDelegate: NSObject, URLSessionDelegate { 11 | 12 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 13 | challenge.trustServer { challangeDisposition, credential in 14 | completionHandler(challangeDisposition,credential) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ProductClean/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'ProductClean' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for ProductClean 9 | pod 'Kingfisher', '~> 7.0' 10 | target 'ProductCleanSnapshotTests' do 11 | inherit! :search_paths 12 | # Pods for testing 13 | pod 'SnapshotTesting' 14 | end 15 | 16 | target 'ProductCleanTests' do 17 | inherit! :search_paths 18 | # Pods for testing 19 | end 20 | 21 | target 'ProductCleanUITests' do 22 | # Pods for testing 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/Mock/MockData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockData.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | import Foundation 8 | @testable import ProductClean 9 | 10 | class MockData { 11 | 12 | static let productList: [ProductListItemViewModel] = [.init(id: 1, title: "Title 1", description: "Description 1", price: "$100", category: "iPhone", image: imageURLString)] 13 | 14 | static let imageURLString = "" 15 | 16 | } 17 | 18 | struct SnapshotTolerance { 19 | static let perPixelTolerance: CGFloat = 6/256 20 | static let overallTolerance: CGFloat = 0.02 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/Mock/MockNetworkSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSession.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | @testable import ProductClean 10 | 11 | class MockNetworkSessionManager: NetworkSessionManager { 12 | var data: Data? 13 | var response: URLResponse? 14 | var error: Error? 15 | 16 | func request(with config: NetworkConfigurable, request: NetworkRequest) async throws -> (Data?, URLResponse?) { 17 | if let error = self.error { 18 | throw error 19 | } 20 | return (data, response) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Application/ProductCleanApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductCleanApp.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | import Kingfisher 10 | @main 11 | struct ProductCleanApp: App { 12 | 13 | private let appDIContainer = AppDIContainer() 14 | private let imageAuthenticationChallenge = ImageDownloadAuthenticationChallenge() 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | appDIContainer.productListView 19 | } 20 | } 21 | 22 | init() { 23 | ImageDownloader.default.authenticationChallengeResponder = imageAuthenticationChallenge 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ProductItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductItem.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductItemView: View { 11 | 12 | var item: ProductListItemViewModel 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | HeaderImageView(urlString: item.image, height: 150) 17 | Text(item.title).font(.title) 18 | Text(item.price) 19 | .foregroundStyle(.red) 20 | .font(.title2) 21 | } 22 | .preferredColorScheme(.light) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Domain/UseCase/ProductListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListUseCase.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductListUseCase { 11 | func fetchProductList() async throws -> [ProductDomainListDTO] 12 | } 13 | 14 | final class DefaultProductListUseCase: ProductListUseCase { 15 | 16 | private let repository: ProductListRepository 17 | 18 | init(repository: ProductListRepository) { 19 | self.repository = repository 20 | } 21 | 22 | func fetchProductList() async throws -> [ProductDomainListDTO] { 23 | try await repository.fetchProductList() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/Mock/MockNetworkRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNetworkRequest.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | @testable import ProductClean 10 | 11 | class MockNetworkRequest: NetworkRequest { 12 | var path = "/products" 13 | var method = HTTPMethod.get 14 | var headerParameters: [String : String] = ["Content-Type":"application/json"] 15 | var queryParameters: [String : Any] = [:] 16 | var bodyParameters: [String : Any] = [:] 17 | } 18 | 19 | class MockApiDataNetworkConfig: NetworkConfigurable { 20 | var baseURL: String = "dummyjson.com" 21 | var headers: [String : String] = ["Content-Type":"application/json"] 22 | } 23 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Util/ImageDownloadAuthChallenge/ImageDownloadAuthenticationChallenge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDownloadAuthenticationChallenge.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 27/02/24. 6 | // 7 | 8 | import Foundation 9 | import Kingfisher 10 | 11 | final class ImageDownloadAuthenticationChallenge : AuthenticationChallengeResponsible { 12 | 13 | func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 14 | challenge.trustServer { challangeDisposition, credential in 15 | completionHandler(challangeDisposition,credential) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Util/Extensions/Double+CurrencyAmount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+CurrencyAmount.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 26/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | 12 | /// It converts a double amount to a string with currency 13 | /// For example double value -> 100, return value -> "$100" 14 | func getAmountWithCurrency() -> String { 15 | let numberFormatter = NumberFormatter() 16 | numberFormatter.numberStyle = .currency 17 | numberFormatter.maximumFractionDigits = 0 18 | numberFormatter.locale = Locale(identifier: AppConstant.currencyIndentifier) 19 | return numberFormatter.string(from: NSNumber(value: self)) ?? "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/HeaderImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderImageView.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | import Kingfisher 10 | 11 | struct HeaderImageView: View { 12 | 13 | let urlString: String 14 | let height: CGFloat 15 | 16 | var body: some View { 17 | KFImage(URL(string: urlString)) 18 | .placeholder { 19 | Rectangle().foregroundColor(.gray) 20 | } 21 | .resizable() 22 | .scaledToFit() 23 | .frame(maxWidth: .infinity) 24 | .aspectRatio(120.0/63.0, contentMode: .fit) 25 | .cornerRadius(4.0) 26 | .preferredColorScheme(.light) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/ErrorViewSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorViewSnapshotTest.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ErrorViewSnapshotTests: XCTestCase { 12 | 13 | var errorVC: UIViewController! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | errorVC = ErrorView(errorTitle: "Error", errorDescription: "Error Description", retryAction: {}).toViewController() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | errorVC = nil 23 | } 24 | 25 | func testLaunchForErrorView() { 26 | errorVC.performSnapshotTests(named: "ErrorView") 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/ProductDetailsViewSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDetailsSnapshotTests.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductDetailsSnapshotTests: XCTestCase { 12 | 13 | var productDetailsVC: UIViewController! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | productDetailsVC = ProductDetailsView(item: MockData.productList[0]).toViewController() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | productDetailsVC = nil 23 | } 24 | 25 | func testLaunchForProductDetailsView() { 26 | productDetailsVC.performSnapshotTests(named: "ProductDetailsView") 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/HeaderImageViewSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderImageViewSnapshotTests.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class HeaderImageViewSnapshotTests: XCTestCase { 12 | 13 | var headerImageVC: UIViewController! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | headerImageVC = HeaderImageView(urlString: MockData.imageURLString, height: 300).toViewController() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | headerImageVC = nil 23 | } 24 | 25 | 26 | func testLaunchForHeaderImageView() { 27 | headerImageVC.performSnapshotTests(named: "HeaderImageView") 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/ProductItemViewSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListCellSnapshotTests.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductItemViewSnapshotTests: XCTestCase { 12 | 13 | var productListCellVC: UIViewController! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | productListCellVC = ProductItemView(item: MockData.productList[0]).toViewController() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | productListCellVC = nil 23 | } 24 | 25 | func testLaunchForProductListCellView() { 26 | productListCellVC.performSnapshotTests(named: "ProductListCellView") 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ProductListLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListLayout.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductListLayout: View { 11 | 12 | let items: [ProductListItemViewModel] 13 | 14 | var body: some View { 15 | List { 16 | ForEach(items, id: \.id) { item in 17 | NavigationLink(value: item) { 18 | ProductItemView(item: item) 19 | } 20 | } 21 | } 22 | .navigationDestination(for: ProductListItemViewModel.self, destination: { item in 23 | /// Move to Product Details View 24 | ProductDetailsView(item: item) 25 | }) 26 | .preferredColorScheme(.light) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyTracking 17 | 18 | NSPrivacyTrackingDomains 19 | 20 | 21 | NSPrivacyCollectedDataTypes 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Util/Extensions/URLAuthenticationChallenge+TrustServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLAuthenticationChallenge+TrustServer.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 29/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLAuthenticationChallenge { 11 | 12 | func trustServer(completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 13 | if self.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { 14 | if let serverTrust = self.protectionSpace.serverTrust { 15 | let credential = URLCredential(trust: serverTrust) 16 | completionHandler(.useCredential, credential) 17 | return 18 | } 19 | } 20 | completionHandler(.performDefaultHandling, nil) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 5 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 5 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Data/Services/ProductListService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListService.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductListService { 11 | func fetchProductListFromNetwork() async throws -> ProductPageDataListDTO 12 | } 13 | 14 | final class DefaultProductListService: ProductListService { 15 | 16 | private let apiDataService: DataTransferService 17 | 18 | init(apiDataService: DataTransferService) { 19 | self.apiDataService = apiDataService 20 | } 21 | 22 | func fetchProductListFromNetwork() async throws -> ProductPageDataListDTO { 23 | let productListNetworkRequest = DefaultNetworkRequest(path: APIEndpoint.products,method: .get) 24 | return try await apiDataService.request(request: productListNetworkRequest) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Application/AppDIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDIContainer.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AppDIContainer { 11 | 12 | lazy private var apiDataTransferService: DataTransferService = { 13 | let config = ApiDataNetworkConfig(baseURL: AppConfiguration.baseURL) 14 | let sessionManager = DefaultNetworkSessionManager(session: SharedURLSession.shared) 15 | let networkManager = DefaultNetworkManager(config: config, sessionManager: sessionManager) 16 | return DefaultDataTransferService(networkManager: networkManager) 17 | }() 18 | 19 | lazy var productListView: ProductListView = { 20 | let productsModule = ProductsModule(apiDataTransferService: apiDataTransferService) 21 | return productsModule.generateProductListView() 22 | }() 23 | } 24 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/ProductListLayoutSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListLayoutSnapshotTests.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | //import FBSnapshotTestCase 10 | import SwiftUI 11 | @testable import ProductClean 12 | 13 | final class ProductListLayoutSnapshotTests: XCTestCase { 14 | 15 | var productListLayoutVC: UIViewController! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | productListLayoutVC = ProductListLayout(items: MockData.productList).toViewController() 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | productListLayoutVC = nil 25 | } 26 | 27 | func testLaunchForProductListLayoutView() { 28 | productListLayoutVC.performSnapshotTests(named: "ProductListLayoutView") 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleIdentifier 8 | ${PRODUCT_BUNDLE_IDENTIFIER} 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | ${PRODUCT_NAME} 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 7.11.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ProductDetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDetailsView.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductDetailsView: View { 11 | 12 | let item: ProductListItemViewModel 13 | 14 | var body: some View { 15 | ScrollView { 16 | VStack(alignment: .leading) { 17 | HeaderImageView(urlString: item.image, height: 300) 18 | Text(item.title) 19 | .font(.title) 20 | Text(item.price) 21 | .font(.title2) 22 | Text(item.description) 23 | .font(.title3) 24 | } 25 | .padding(10) 26 | .navigationTitle(item.category) 27 | .navigationBarTitleDisplayMode(.inline) 28 | }.preferredColorScheme(.light) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | opt_in_rules: # some rules are only opt-in 4 | - empty_count 5 | # Find all the available rules by running: 6 | # swiftlint rules 7 | 8 | included: # paths to include during linting. `--path` is ignored if present. 9 | - PlistDesign 10 | excluded: # paths to ignore during linting. Takes precedence over `included`. 11 | - Carthage 12 | - Pods 13 | 14 | # configurable rules can be customized from this configuration file 15 | # rules that have both warning and error levels, can set just the warning level 16 | # implicitly 17 | line_length: 500 18 | # they can set both implicitly with an array 19 | type_body_length: 20 | - 300 # warning 21 | - 400 # error 22 | type_name: 23 | min_length: 3 # only warning 24 | excluded: # No regex support available for this 25 | - _JWT 26 | identifier_name: 27 | min_length: # only min_length 28 | warning: 3 # only error 29 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) 30 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanUITests/ProductCleanUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductCleanUITestsLaunchTests.swift 3 | // ProductCleanUITests 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ProductCleanUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 7.11.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.9.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanTests/Pods-ProductCleanTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ProductClean/ProductClean.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "ios-snapshot-test-case", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/uber/ios-snapshot-test-case.git", 7 | "state" : { 8 | "revision" : "7b10770333a961be6e5a41c9ce04b8c6d3990126", 9 | "version" : "8.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "sdwebimage", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/SDWebImage/SDWebImage.git", 16 | "state" : { 17 | "revision" : "b11493f76481dff17ac8f45274a6b698ba0d3af5", 18 | "version" : "5.18.11" 19 | } 20 | }, 21 | { 22 | "identity" : "sdwebimageswiftui", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git", 25 | "state" : { 26 | "revision" : "261b6cec35686d2dc192b809ab50742b4502a73b", 27 | "version" : "2.2.6" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | This is an iOS application which fetches product list from a public api and shows them in a list. By clicking on each item in the list it goes to the product details page. 4 | This project is built using Clean architecture with MVVM. SwiftUI has been used for UI. 5 | 6 | ## Screenshots 7 | 8 | ProductList 9 | 10 | ProductDetails 11 | 12 | ## Test Coverage 13 | 14 | ### Unit Tests 15 | Application test coverage is 85% and above. 16 | 17 | CodeCoverage 18 | 19 | 20 | ### Snapshot Tests 21 | All views have been covered in snapshot tests. 22 | 23 | Snapshot test cases are covered for iPhone 15, iOS 17.2 24 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanSnapshotTests/ProductListViewSnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductCleanSnapshotTests.swift 3 | // ProductCleanSnapshotTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductListViewSnapshotTests: XCTestCase { 12 | 13 | let viewModel = ProductListViewModelMock() 14 | var productListVC: UIViewController! 15 | let products = MockData.productList 16 | 17 | override func setUp() { 18 | super.setUp() 19 | productListVC = ProductListView(viewModel: viewModel).toViewController() 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | productListVC = nil 25 | } 26 | 27 | func testLaunchForProductListViewSuccess() { 28 | viewModel.products = products 29 | productListVC.performSnapshotTests(named: "ProductListViewSuccess") 30 | } 31 | 32 | func testLaunchForProductListViewFailure() { 33 | viewModel.isError = true 34 | productListVC.performSnapshotTests(named: "ProductListViewFailure") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | extension Snapshotting where Value == String, Format == String { 5 | /// A snapshot strategy for comparing strings based on equality. 6 | public static let lines = Snapshotting(pathExtension: "txt", diffing: .lines) 7 | } 8 | 9 | extension Diffing where Value == String { 10 | /// A line-diffing strategy for UTF-8 text. 11 | public static let lines = Diffing( 12 | toData: { Data($0.utf8) }, 13 | fromData: { String(decoding: $0, as: UTF8.self) } 14 | ) { old, new in 15 | guard old != new else { return nil } 16 | let hunks = chunk(diff: SnapshotTesting.diff( 17 | old.split(separator: "\n", omittingEmptySubsequences: false).map(String.init), 18 | new.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) 19 | )) 20 | let failure = hunks 21 | .flatMap { [$0.patchMark] + $0.lines } 22 | .joined(separator: "\n") 23 | let attachment = XCTAttachment(data: Data(failure.utf8), uniformTypeIdentifier: "public.patch-file") 24 | return (failure, [attachment]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Wei Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Point-Free, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/Mock/MockData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockData.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import Foundation 9 | @testable import ProductClean 10 | 11 | final class MockData { 12 | 13 | static var mockURL = URL(string: "https://dummyjson.com/products")! 14 | static var productList: [ProductDomainListDTO] { 15 | return domainProducts 16 | } 17 | 18 | static var domainProducts: [ProductDomainListDTO] { 19 | return productPage.products.map { $0.toDomain() } 20 | } 21 | 22 | static var productPage: ProductPageDataListDTO { 23 | try! JSONDecoder().decode(ProductPageDataListDTO.self, from: productsRawData) 24 | } 25 | 26 | static var productsRawData: Data { 27 | loadJsonData("Products") 28 | } 29 | 30 | private static func loadJsonData(_ fromFile: String) -> Data { 31 | let path = Bundle(for: self).path(forResource: fromFile, ofType: "json") 32 | let jsonString = try! String(contentsOfFile: path!, encoding: .utf8) 33 | let data = jsonString.data(using: .utf8)! 34 | return data 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ErrorView: View { 11 | 12 | let errorTitle: String 13 | let errorDescription: String 14 | let retryAction: () -> Void 15 | 16 | var body: some View { 17 | VStack { 18 | Image(systemName: AppConstant.errorImage) 19 | .foregroundColor(.gray) 20 | .padding(5) 21 | Text(errorTitle) 22 | .font(.title) 23 | .bold() 24 | .multilineTextAlignment(.center) 25 | .padding(5) 26 | Text(errorDescription) 27 | .font(.caption) 28 | .multilineTextAlignment(.center) 29 | .foregroundColor(.gray) 30 | .padding(5) 31 | Button(AppConstant.retry) { 32 | retryAction() 33 | } 34 | .bold() 35 | } 36 | .padding(50) 37 | .animation(.easeInOut, value: 0.5) 38 | .preferredColorScheme(.light) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/NSViewController.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Cocoa 3 | 4 | extension Snapshotting where Value == NSViewController, Format == NSImage { 5 | /// A snapshot strategy for comparing view controller views based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image() 8 | } 9 | 10 | /// A snapshot strategy for comparing view controller views based on pixel equality. 11 | /// 12 | /// - Parameters: 13 | /// - precision: The percentage of pixels that must match. 14 | /// - size: A view size override. 15 | public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { 16 | return Snapshotting.image(precision: precision, size: size).pullback { $0.view } 17 | } 18 | } 19 | 20 | extension Snapshotting where Value == NSViewController, Format == String { 21 | /// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies. 22 | public static var recursiveDescription: Snapshotting { 23 | return Snapshotting.recursiveDescription.pullback { $0.view } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/CaseIterable.swift: -------------------------------------------------------------------------------- 1 | extension Snapshotting where Value: CaseIterable, Format == String { 2 | /// A strategy for snapshotting the output for every input of a function. The format of the snapshot 3 | /// is a comma-separated value (CSV) file that shows the mapping of inputs to outputs. 4 | /// 5 | /// Parameter witness: A snapshotting value on the output of the function to be snapshot. 6 | /// Returns: A snapshot strategy on functions (Value) -> A that feeds every possible input into the 7 | /// function and records the output into a CSV file. 8 | public static func `func`(into witness: Snapshotting) -> Snapshotting<(Value) -> A, Format> { 9 | var snapshotting = Snapshotting.lines.asyncPullback { (f: (Value) -> A) in 10 | Value.allCases.map { input in 11 | witness.snapshot(f(input)) 12 | .map { (input, $0) } 13 | } 14 | .sequence() 15 | .map { rows in 16 | rows.map { "\"\($0)\",\"\($1)\"" } 17 | .joined(separator: "\n") 18 | } 19 | } 20 | 21 | snapshotting.pathExtension = "csv" 22 | 23 | return snapshotting 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanUITests/ProductCleanUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductCleanUITests.swift 3 | // ProductCleanUITests 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ProductCleanUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/NetworkSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkSessionManager.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | final class DefaultNetworkSessionManager: NetworkSessionManager { 12 | 13 | private let session: URLSessionProtocol 14 | private let requestGenerator: URLRequestGenerator 15 | init(session: URLSessionProtocol = URLSession.shared, 16 | requestGenerator: URLRequestGenerator = DefaultURLRequestGenerator()) { 17 | self.session = session 18 | self.requestGenerator = requestGenerator 19 | } 20 | 21 | /// Method to get data and response from URLSession 22 | /// - Parameters: 23 | /// - config: Network config 24 | /// - request: Network request 25 | /// - Returns: Data and Response 26 | func request(with config: NetworkConfigurable, request: NetworkRequest) async throws -> (Data?, URLResponse?) { 27 | let urlRequest = try requestGenerator.generateURLRequest(with: config, from: request) 28 | do { 29 | return try await session.asyncData(for: urlRequest) 30 | } catch { 31 | throw NetworkError.resolve(error: error) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ProductListView_Previews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListView_Previews.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ProductListView_Previews { 11 | 12 | static func getViewModel() -> ProductListViewModelMock { 13 | return ProductListViewModelMock() 14 | } 15 | 16 | class ProductListViewModelMock: ProductListViewModelProtocol { 17 | 18 | func shouldShowLoader() -> Bool {isEmpty && isError} 19 | 20 | var products: [ProductListItemViewModel] = [.init(id: 1, title: "Title 1", description: "Description 1", price: "$100", category: "iPhone", image: "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"), 21 | .init(id: 2, title: "Title 2", description: "Description 2", price: "$200", category: "iPhone", image: "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg")] 22 | var isEmpty: Bool { return products.isEmpty } 23 | var isError: Bool = false 24 | var error: String = "Error" 25 | var title: String = "Products" 26 | func fetchProducts() async {} 27 | } 28 | } 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Extensions/Wait.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | extension Snapshotting { 5 | /// Transforms an existing snapshot strategy into one that waits for some amount of time before taking the snapshot. This can be useful for waiting for animations to complete or for UIKit events to finish (i.e. waiting for a UINavigationController to push a child onto the stack). 6 | /// - Parameters: 7 | /// - duration: The amount of time to wait before taking the snapshot. 8 | /// - strategy: The snapshot to invoke after the specified amount of time has passed. 9 | public static func wait( 10 | for duration: TimeInterval, 11 | on strategy: Snapshotting 12 | ) -> Snapshotting { 13 | return Snapshotting( 14 | pathExtension: strategy.pathExtension, 15 | diffing: strategy.diffing, 16 | asyncSnapshot: { value in 17 | Async { callback in 18 | let expectation = XCTestExpectation(description: "Wait") 19 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 20 | expectation.fulfill() 21 | } 22 | _ = XCTWaiter.wait(for: [expectation], timeout: duration + 1) 23 | strategy.snapshot(value).run(callback) 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting 3 | ENABLE_BITCODE = NO 4 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 7 | OTHER_LDFLAGS = $(inherited) -framework "XCTest" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 12 | PODS_ROOT = ${SRCROOT} 13 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SnapshotTesting 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 16 | SKIP_INSTALL = YES 17 | SWIFT_INCLUDE_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" 18 | SYSTEM_FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" 19 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 20 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/SnapshotTesting/SnapshotTesting.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting 3 | ENABLE_BITCODE = NO 4 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 7 | OTHER_LDFLAGS = $(inherited) -framework "XCTest" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 12 | PODS_ROOT = ${SRCROOT} 13 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SnapshotTesting 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 16 | SKIP_INSTALL = YES 17 | SWIFT_INCLUDE_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" 18 | SYSTEM_FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" 19 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 20 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/ProductsModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListModule.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class ProductsModule { 11 | 12 | private let apiDataTransferService: DataTransferService 13 | 14 | init(apiDataTransferService: DataTransferService) { 15 | self.apiDataTransferService = apiDataTransferService 16 | } 17 | 18 | func generateProductListView() -> ProductListView { 19 | ProductListView(viewModel: generateProductListViewModel()) 20 | } 21 | 22 | private func generateProductListViewModel() -> ProductListViewModel { 23 | ProductListViewModel(useCase: generateProductListUseCase()) 24 | } 25 | 26 | private func generateProductListUseCase() -> ProductListUseCase { 27 | DefaultProductListUseCase(repository: generateProductListRepository()) 28 | } 29 | 30 | private func generateProductListRepository() -> ProductListRepository { 31 | DefaultProductListRepository(service: generateProductListService()) 32 | } 33 | 34 | private func generateProductListService() -> ProductListService { 35 | DefaultProductListService(apiDataService: apiDataTransferService) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkError.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkError: Error { 11 | case badURL 12 | case badHostname 13 | case failed 14 | case noResponse 15 | case noData 16 | case unableToDecode 17 | case notConnected 18 | case generic 19 | var description: String { 20 | switch self { 21 | case .badURL: return "Bad URL" 22 | case .badHostname: return "A server with the specified hostname could not be found" 23 | case .failed: return "Network Request Failed" 24 | case .noResponse: return "No response" 25 | case .noData: return "No Data" 26 | case .unableToDecode: return "Response can't be decoded" 27 | case .notConnected: return "The internet connection appears to be offline" 28 | case .generic: return "Something went wrong" 29 | } 30 | } 31 | 32 | static func resolve(error: Error) -> NetworkError { 33 | let code = URLError.Code(rawValue: (error as NSError).code) 34 | switch code { 35 | case .notConnectedToInternet: return .notConnected 36 | case .cannotFindHost: return .badHostname 37 | default: return .generic 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/NetworkRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkRequest.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HTTPMethod: String { 11 | case get = "GET" 12 | case post = "POST" 13 | case put = "PUT" 14 | case patch = "PATCH" 15 | case delete = "DELETE" 16 | } 17 | 18 | protocol NetworkRequest { 19 | var path: String {get set} 20 | var method: HTTPMethod {get set} 21 | var headerParameters: [String: String] {get set} 22 | var queryParameters: [String: Any] {get set} 23 | var bodyParameters: [String: Any] {get set} 24 | } 25 | 26 | final class DefaultNetworkRequest: NetworkRequest { 27 | 28 | var path: String 29 | var method: HTTPMethod 30 | var headerParameters: [String: String] 31 | var queryParameters: [String: Any] 32 | var bodyParameters: [String: Any] 33 | init(path: String, 34 | method: HTTPMethod = .get, 35 | headerParameters: [String: String] = [:], 36 | queryParameters: [String: Any] = [:], 37 | bodyParameters: [String: Any] = [:]) { 38 | self.path = path 39 | self.method = method 40 | self.headerParameters = headerParameters 41 | self.queryParameters = queryParameters 42 | self.bodyParameters = bodyParameters 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Data/Entity/ProductDataListDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDataListDTO.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | struct ProductPageDataListDTO: Decodable { 10 | let products: [ProductDataListDTO] 11 | } 12 | struct ProductDataListDTO: Decodable { 13 | let productId: Int 14 | let title: String? 15 | let description: String? 16 | let price: Double? 17 | let discountPercentage: Double? 18 | let rating: Double? 19 | let stock: Int? 20 | let brand: String? 21 | let category: String? 22 | let thumbnail: String? 23 | let images: [String]? 24 | enum CodingKeys: String, CodingKey { 25 | case productId = "id" 26 | case title 27 | case description 28 | case price 29 | case discountPercentage 30 | case rating 31 | case stock 32 | case brand 33 | case category 34 | case thumbnail 35 | case images 36 | } 37 | } 38 | 39 | extension ProductDataListDTO { 40 | 41 | func toDomain() -> ProductDomainListDTO { 42 | .init(productId: productId, 43 | title: title ?? "", 44 | description: description ?? "", 45 | price: price ?? 0, 46 | category: category ?? "", 47 | thumbnail: thumbnail ?? "") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/DataTransferService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTransferService.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 25/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DataTransferService { 11 | func request(request: NetworkRequest) async throws -> T 12 | } 13 | 14 | final class DefaultDataTransferService: DataTransferService { 15 | 16 | private let networkManager: NetworkManager 17 | init(networkManager: NetworkManager) { 18 | self.networkManager = networkManager 19 | } 20 | 21 | /// Method to fetch data from Network Manager and Decode the data using decode method 22 | /// - Parameter request: Network request 23 | /// - Returns: Decodable type object 24 | func request(request: NetworkRequest) async throws -> T where T : Decodable { 25 | let data = try await networkManager.fetch(request: request) 26 | return try decode(data: data) 27 | } 28 | 29 | /// Method to decode data using JSONDecoder 30 | /// - Parameter data: Data 31 | /// - Returns: Decodable type object 32 | func decode(data: Data) throws -> T where T : Decodable { 33 | do { 34 | let decodedData = try JSONDecoder().decode(T.self, from: data) 35 | return decodedData 36 | } catch { 37 | throw NetworkError.unableToDecode 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Kingfisher 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2019 Wei Wang 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | 29 | Generated by CocoaPods - https://cocoapods.org 30 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/URLRequestCreaterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequestCreaterTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 21/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class URLRequestCreaterTests: XCTestCase { 12 | 13 | var requestGenerator: URLRequestGenerator! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | requestGenerator = DefaultURLRequestGenerator() 18 | } 19 | 20 | override func tearDown() { 21 | requestGenerator = nil 22 | super.tearDown() 23 | } 24 | 25 | func testURLRequest() { 26 | do { 27 | let urlRequest = try requestGenerator.generateURLRequest(with: MockApiDataNetworkConfig(), from: MockNetworkRequest()) 28 | XCTAssertEqual(urlRequest.url?.host, "dummyjson.com") 29 | XCTAssertEqual(urlRequest.url?.scheme, "https") 30 | XCTAssertEqual(urlRequest.url?.path, "/products") 31 | XCTAssertEqual(urlRequest.url, URL(string: "https://dummyjson.com/products?")) 32 | XCTAssertEqual(urlRequest.httpMethod, HTTPMethod.get.rawValue) 33 | XCTAssertEqual(urlRequest.allHTTPHeaderFields, ["Content-Type":"application/json"]) 34 | XCTAssertNil(urlRequest.httpBody) 35 | } catch { 36 | XCTFail("Request Failed unexpectedly") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SnapshotTesting 5 | 6 | MIT License 7 | 8 | Copyright (c) 2019 Point-Free, Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/ProductListRepositoryTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListRepositoryTest.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductListRepositoryTest: XCTestCase { 12 | 13 | var productRepository: ProductListRepository! 14 | var mockProductService: MockProductListService! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | mockProductService = MockProductListService() 19 | productRepository = DefaultProductListRepository(service: mockProductService) 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | productRepository = nil 25 | mockProductService = nil 26 | } 27 | 28 | func testProductRepositorySuccess() async throws { 29 | mockProductService.response = MockData.productPage 30 | let products = try await productRepository.fetchProductList() 31 | XCTAssertNotNil(products) 32 | } 33 | 34 | func testProductRepositoryFailure() async throws { 35 | mockProductService.error = NetworkError.failed 36 | do { 37 | _ = try await productRepository.fetchProductList() 38 | XCTFail("Success not expected") 39 | } catch { 40 | XCTAssertEqual(error as! NetworkError, NetworkError.failed) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/ProductListUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListUseCaseTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductListUseCaseTest: XCTestCase { 12 | 13 | var productUseCase: ProductListUseCase! 14 | var mockProductRepository: MockProductListRepository! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | mockProductRepository = MockProductListRepository() 19 | productUseCase = DefaultProductListUseCase(repository: mockProductRepository) 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | productUseCase = nil 25 | mockProductRepository = nil 26 | } 27 | 28 | func testProductUseCaseSuccess() async throws { 29 | mockProductRepository.response = MockData.domainProducts 30 | let products = try await productUseCase.fetchProductList() 31 | XCTAssertNotNil(products) 32 | } 33 | 34 | func testProductUseCaseFailure() async throws { 35 | mockProductRepository.error = NetworkError.failed 36 | do { 37 | _ = try await productUseCase.fetchProductList() 38 | XCTFail("Success not expected") 39 | } catch { 40 | XCTAssertEqual(error as! NetworkError, NetworkError.failed) 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Diffing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | /// The ability to compare `Value`s and convert them to and from `Data`. 5 | public struct Diffing { 6 | /// Converts a value _to_ data. 7 | public var toData: (Value) -> Data 8 | 9 | /// Produces a value _from_ data. 10 | public var fromData: (Data) -> Value 11 | 12 | /// Compares two values. If the values do not match, returns a failure message and artifacts describing the failure. 13 | public var diff: (Value, Value) -> (String, [XCTAttachment])? 14 | 15 | /// Creates a new `Diffing` on `Value`. 16 | /// 17 | /// - Parameters: 18 | /// - toData: A function used to convert a value _to_ data. 19 | /// - value: A value to convert into data. 20 | /// - fromData: A function used to produce a value _from_ data. 21 | /// - data: Data to convert into a value. 22 | /// - diff: A function used to compare two values. If the values do not match, returns a failure message and artifacts describing the failure. 23 | /// - lhs: A value to compare. 24 | /// - rhs: Another value to compare. 25 | public init( 26 | toData: @escaping (_ value: Value) -> Data, 27 | fromData: @escaping (_ data: Data) -> Value, 28 | diff: @escaping (_ lhs: Value, _ rhs: Value) -> (String, [XCTAttachment])? 29 | ) { 30 | self.toData = toData 31 | self.fromData = fromData 32 | self.diff = diff 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Kingfisher 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2019 Wei Wang 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | 29 | Generated by CocoaPods - https://cocoapods.org 30 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/ProductListServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListServiceTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | import XCTest 8 | @testable import ProductClean 9 | 10 | final class PostServiceTests: XCTestCase { 11 | 12 | var productService: ProductListService! 13 | var mockDataTransferService: MockDataTransferService! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | mockDataTransferService = MockDataTransferService() 18 | productService = DefaultProductListService(apiDataService: mockDataTransferService) 19 | } 20 | 21 | override func tearDown() { 22 | productService = nil 23 | mockDataTransferService = nil 24 | super.tearDown() 25 | } 26 | 27 | func testProductServiceSuccess() async throws { 28 | mockDataTransferService.response = MockData.productPage 29 | let productPage = try await productService.fetchProductListFromNetwork() 30 | XCTAssertEqual(productPage.products.count, 5) 31 | } 32 | 33 | func testProductServiceFailure() async throws { 34 | mockDataTransferService.error = NSError(domain: "FailedError", code: 0) 35 | do { 36 | _ = try await productService.fetchProductListFromNetwork() 37 | XCTFail("Success not expected") 38 | } catch { 39 | XCTAssertNotNil(error) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NetworkSessionManager { 11 | func request(with config: NetworkConfigurable, request: NetworkRequest) async throws -> (Data?, URLResponse?) 12 | } 13 | protocol NetworkManager { 14 | func fetch(request: NetworkRequest) async throws -> Data 15 | } 16 | 17 | final class DefaultNetworkManager: NetworkManager { 18 | 19 | private let config: NetworkConfigurable 20 | private let sessionManager: NetworkSessionManager 21 | 22 | init(config: NetworkConfigurable, 23 | sessionManager: NetworkSessionManager = DefaultNetworkSessionManager()) { 24 | self.config = config 25 | self.sessionManager = sessionManager 26 | } 27 | 28 | /// Method to fetch data from Session Manager and validates the data and response 29 | /// - Parameter request: Network Request 30 | /// - Returns: Data 31 | func fetch(request: NetworkRequest) async throws -> Data { 32 | let (data, response) = try await sessionManager.request(with: config, request: request) 33 | guard let response = response as? HTTPURLResponse else { throw NetworkError.noResponse } 34 | if response.statusCode != 200 { throw NetworkError.failed } 35 | guard let data = data else { throw NetworkError.noData } 36 | return data 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting/SnapshotTesting.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -framework "SnapshotTesting" -framework "XCTest" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting/SnapshotTesting.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -framework "SnapshotTesting" -framework "XCTest" -weak_framework "Combine" -weak_framework "SwiftUI" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Utility/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Box.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2018/3/17. 6 | // Copyright (c) 2019 Wei Wang 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | 28 | class Box { 29 | var value: T 30 | 31 | init(_ value: T) { 32 | self.value = value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/SceneKit.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(macOS) || os(tvOS) 2 | import SceneKit 3 | #if os(macOS) 4 | import Cocoa 5 | #elseif os(iOS) || os(tvOS) 6 | import UIKit 7 | #endif 8 | 9 | #if os(macOS) 10 | extension Snapshotting where Value == SCNScene, Format == NSImage { 11 | /// A snapshot strategy for comparing SceneKit scenes based on pixel equality. 12 | /// 13 | /// - Parameters: 14 | /// - precision: The percentage of pixels that must match. 15 | /// - size: The size of the scene. 16 | public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { 17 | return .scnScene(precision: precision, size: size) 18 | } 19 | } 20 | #elseif os(iOS) || os(tvOS) 21 | extension Snapshotting where Value == SCNScene, Format == UIImage { 22 | /// A snapshot strategy for comparing SceneKit scenes based on pixel equality. 23 | /// 24 | /// - Parameters: 25 | /// - precision: The percentage of pixels that must match. 26 | /// - size: The size of the scene. 27 | public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { 28 | return .scnScene(precision: precision, size: size) 29 | } 30 | } 31 | #endif 32 | 33 | fileprivate extension Snapshotting where Value == SCNScene, Format == Image { 34 | static func scnScene(precision: Float, size: CGSize) -> Snapshotting { 35 | return Snapshotting.image(precision: precision).pullback { scene in 36 | let view = SCNView(frame: .init(x: 0, y: 0, width: size.width, height: size.height)) 37 | view.scene = scene 38 | return view 39 | } 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/SpriteKit.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(macOS) || os(tvOS) 2 | import SpriteKit 3 | #if os(macOS) 4 | import Cocoa 5 | #elseif os(iOS) || os(tvOS) 6 | import UIKit 7 | #endif 8 | 9 | #if os(macOS) 10 | extension Snapshotting where Value == SKScene, Format == NSImage { 11 | /// A snapshot strategy for comparing SpriteKit scenes based on pixel equality. 12 | /// 13 | /// - Parameters: 14 | /// - precision: The percentage of pixels that must match. 15 | /// - size: The size of the scene. 16 | public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { 17 | return .skScene(precision: precision, size: size) 18 | } 19 | } 20 | #elseif os(iOS) || os(tvOS) 21 | extension Snapshotting where Value == SKScene, Format == UIImage { 22 | /// A snapshot strategy for comparing SpriteKit scenes based on pixel equality. 23 | /// 24 | /// - Parameters: 25 | /// - precision: The percentage of pixels that must match. 26 | /// - size: The size of the scene. 27 | public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { 28 | return .skScene(precision: precision, size: size) 29 | } 30 | } 31 | #endif 32 | 33 | fileprivate extension Snapshotting where Value == SKScene, Format == Image { 34 | static func skScene(precision: Float, size: CGSize) -> Snapshotting { 35 | return Snapshotting.image(precision: precision).pullback { scene in 36 | let view = SKView(frame: .init(x: 0, y: 0, width: size.width, height: size.height)) 37 | view.presentScene(scene) 38 | return view 39 | } 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/Views/ProductListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListView.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductListView: View where ViewModel: ProductListViewModelProtocol { 11 | 12 | @ObservedObject private var viewModel: ViewModel 13 | init(viewModel: ViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | 17 | var body: some View { 18 | NavigationStack { 19 | if viewModel.shouldShowLoader() { 20 | ProgressView() 21 | .progressViewStyle(.circular) 22 | } else { 23 | ProductListLayout(items: viewModel.products) 24 | .overlay { 25 | if viewModel.isError { 26 | ErrorView(errorTitle: AppConstant.errorTitle, errorDescription: viewModel.error) { 27 | Task { 28 | await fetchProducts() 29 | } 30 | } 31 | } 32 | } 33 | .navigationTitle(viewModel.title) 34 | } 35 | } 36 | .task { 37 | await fetchProducts() 38 | } 39 | } 40 | 41 | private func fetchProducts() async { 42 | await viewModel.fetchProducts() 43 | } 44 | } 45 | 46 | struct ProductListView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | ProductListView(viewModel: ProductListView_Previews.getViewModel()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Async.swift: -------------------------------------------------------------------------------- 1 | /// A wrapper around an asynchronous operation. 2 | /// 3 | /// Snapshot strategies may utilize this type to create snapshots in an asynchronous fashion. 4 | /// 5 | /// For example, WebKit's `WKWebView` offers a callback-based API for taking image snapshots (`takeSnapshot`). `Async` allows us to build a value that can pass its callback along to the scope in which the image has been created. 6 | /// 7 | /// Async { callback in 8 | /// webView.takeSnapshot(with: nil) { image, error in 9 | /// callback(image!) 10 | /// } 11 | /// } 12 | public struct Async { 13 | public let run: (@escaping (Value) -> Void) -> Void 14 | 15 | /// Creates an asynchronous operation. 16 | /// 17 | /// - Parameters: 18 | /// - run: A function that, when called, can hand a value to a callback. 19 | /// - callback: A function that can be called with a value. 20 | public init(run: @escaping (_ callback: @escaping (Value) -> Void) -> Void) { 21 | self.run = run 22 | } 23 | 24 | /// Wraps a pure value in an asynchronous operation. 25 | /// 26 | /// - Parameter value: A value to be wrapped in an asynchronous operation. 27 | public init(value: Value) { 28 | self.init { callback in callback(value) } 29 | } 30 | 31 | /// Transforms an Async into an Async with a function `(Value) -> NewValue`. 32 | /// 33 | /// - Parameter f: A transformation to apply to the value wrapped by the async value. 34 | public func map(_ f: @escaping (Value) -> NewValue) -> Async { 35 | return .init { callback in 36 | self.run { a in callback(f(a)) } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Products/ProductListViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListViewModelTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class ProductListViewModelTest: XCTestCase { 12 | 13 | var productListViewModel: ProductListViewModel! 14 | var mockProductUseCase: MockProductListUseCase! 15 | 16 | override func setUp() async throws { 17 | try await super.setUp() 18 | mockProductUseCase = MockProductListUseCase() 19 | productListViewModel = ProductListViewModel(useCase: mockProductUseCase) 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | productListViewModel = nil 25 | mockProductUseCase = nil 26 | } 27 | 28 | func testProductListViewModelSuccess() async throws { 29 | XCTAssertTrue(productListViewModel.shouldShowLoader()) 30 | mockProductUseCase.response = MockData.productList 31 | await productListViewModel.fetchProducts() 32 | XCTAssertTrue(productListViewModel.products.count == 5) 33 | XCTAssertEqual(productListViewModel.products.first?.price, "$549") 34 | XCTAssertFalse(productListViewModel.shouldShowLoader()) 35 | } 36 | 37 | func testProductListViewModelFailure() async throws { 38 | mockProductUseCase.error = NetworkError.failed 39 | await productListViewModel.fetchProducts() 40 | XCTAssertTrue(productListViewModel.products.count == 0) 41 | XCTAssertNotNil(productListViewModel.error) 42 | XCTAssertEqual(productListViewModel.error, NetworkError.failed.description) 43 | XCTAssertTrue(productListViewModel.isError) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Snapshotting where Value: Encodable, Format == String { 4 | /// A snapshot strategy for comparing encodable structures based on their JSON representation. 5 | @available(iOS 11.0, macOS 10.13, tvOS 11.0, *) 6 | public static var json: Snapshotting { 7 | let encoder = JSONEncoder() 8 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 9 | return .json(encoder) 10 | } 11 | 12 | /// A snapshot strategy for comparing encodable structures based on their JSON representation. 13 | /// 14 | /// - Parameter encoder: A JSON encoder. 15 | public static func json(_ encoder: JSONEncoder) -> Snapshotting { 16 | var snapshotting = SimplySnapshotting.lines.pullback { (encodable: Value) in 17 | try! String(decoding: encoder.encode(encodable), as: UTF8.self) 18 | } 19 | snapshotting.pathExtension = "json" 20 | return snapshotting 21 | } 22 | 23 | /// A snapshot strategy for comparing encodable structures based on their property list representation. 24 | public static var plist: Snapshotting { 25 | let encoder = PropertyListEncoder() 26 | encoder.outputFormat = .xml 27 | return .plist(encoder) 28 | } 29 | 30 | /// A snapshot strategy for comparing encodable structures based on their property list representation. 31 | /// 32 | /// - Parameter encoder: A property list encoder. 33 | public static func plist(_ encoder: PropertyListEncoder) -> Snapshotting { 34 | var snapshotting = SimplySnapshotting.lines.pullback { (encodable: Value) in 35 | try! String(decoding: encoder.encode(encodable), as: UTF8.self) 36 | } 37 | snapshotting.pathExtension = "plist" 38 | return snapshotting 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/NetworkSessionManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkSessionManagerTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class NetworkSessionManagerTests: XCTestCase { 12 | 13 | var networkSessionManger: NetworkSessionManager! 14 | var mockURLSession: MockURLSession! 15 | var response: HTTPURLResponse { 16 | return HTTPURLResponse(url: URL(string: "/posts")!, 17 | statusCode: 200, 18 | httpVersion: nil, 19 | headerFields: nil)! 20 | } 21 | 22 | override func setUp() { 23 | super.setUp() 24 | mockURLSession = MockURLSession() 25 | networkSessionManger = DefaultNetworkSessionManager(session: mockURLSession) 26 | } 27 | 28 | override func tearDown() { 29 | super.tearDown() 30 | networkSessionManger = nil 31 | } 32 | 33 | func testRequestSuccessResponse() async throws { 34 | mockURLSession.data = MockData.productsRawData 35 | mockURLSession.urlResponse = response 36 | do { 37 | let (data,response) = try await mockURLSession.asyncData(for: URLRequest(url: MockData.mockURL)) 38 | XCTAssertNotNil(data) 39 | XCTAssertNotNil(response) 40 | } catch { 41 | XCTFail("Should not fail") 42 | } 43 | } 44 | 45 | func testFailerCase() async throws { 46 | mockURLSession.error = NSError(domain: "Failed", code: 0) 47 | do { 48 | let (data,response) = try await mockURLSession.asyncData(for: URLRequest(url: MockData.mockURL)) 49 | XCTFail("Should not succeed") 50 | } catch { 51 | XCTAssertNotNil(error) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/CALayer.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Cocoa 3 | 4 | extension Snapshotting where Value == CALayer, Format == NSImage { 5 | /// A snapshot strategy for comparing layers based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image(precision: 1) 8 | } 9 | 10 | /// A snapshot strategy for comparing layers based on pixel equality. 11 | /// 12 | /// - Parameter precision: The percentage of pixels that must match. 13 | public static func image(precision: Float) -> Snapshotting { 14 | return SimplySnapshotting.image(precision: precision).pullback { layer in 15 | let image = NSImage(size: layer.bounds.size) 16 | image.lockFocus() 17 | let context = NSGraphicsContext.current!.cgContext 18 | layer.setNeedsLayout() 19 | layer.layoutIfNeeded() 20 | layer.render(in: context) 21 | image.unlockFocus() 22 | return image 23 | } 24 | } 25 | } 26 | #elseif os(iOS) || os(tvOS) 27 | import UIKit 28 | 29 | extension Snapshotting where Value == CALayer, Format == UIImage { 30 | /// A snapshot strategy for comparing layers based on pixel equality. 31 | public static var image: Snapshotting { 32 | return .image() 33 | } 34 | 35 | /// A snapshot strategy for comparing layers based on pixel equality. 36 | /// 37 | /// - Parameter precision: The percentage of pixels that must match. 38 | public static func image(precision: Float = 1, traits: UITraitCollection = .init()) 39 | -> Snapshotting { 40 | return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in 41 | renderer(bounds: layer.bounds, for: traits).image { ctx in 42 | layer.setNeedsLayout() 43 | layer.layoutIfNeeded() 44 | layer.render(in: ctx.cgContext) 45 | } 46 | } 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Utility/Runtime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Runtime.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2018/10/12. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? { 30 | if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { // swift 5.3 fixed this issue (https://github.com/apple/swift/issues/46456) 31 | return objc_getAssociatedObject(object, key) as? T 32 | } else { 33 | return objc_getAssociatedObject(object, key) as AnyObject as? T 34 | } 35 | } 36 | 37 | func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { 38 | objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 39 | } 40 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | import UIKit 3 | 4 | extension Snapshotting where Value == UIBezierPath, Format == UIImage { 5 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image() 8 | } 9 | 10 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 11 | /// 12 | /// - Parameter precision: The percentage of pixels that must match. 13 | public static func image(precision: Float = 1, scale: CGFloat = 1) -> Snapshotting { 14 | return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in 15 | let bounds = path.bounds 16 | let format: UIGraphicsImageRendererFormat 17 | if #available(iOS 11.0, tvOS 11.0, *) { 18 | format = UIGraphicsImageRendererFormat.preferred() 19 | } else { 20 | format = UIGraphicsImageRendererFormat.default() 21 | } 22 | format.scale = scale 23 | return UIGraphicsImageRenderer(bounds: bounds, format: format).image { ctx in 24 | path.fill() 25 | } 26 | } 27 | } 28 | } 29 | 30 | @available(iOS 11.0, tvOS 11.0, *) 31 | extension Snapshotting where Value == UIBezierPath, Format == String { 32 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 33 | public static var elementsDescription: Snapshotting { 34 | Snapshotting.elementsDescription.pullback { path in path.cgPath } 35 | } 36 | 37 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 38 | /// 39 | /// - Parameter numberFormatter: The number formatter used for formatting points. 40 | public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting { 41 | Snapshotting.elementsDescription( 42 | numberFormatter: numberFormatter 43 | ).pullback { path in path.cgPath } 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/NSView.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Cocoa 3 | 4 | extension Snapshotting where Value == NSView, Format == NSImage { 5 | /// A snapshot strategy for comparing views based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image() 8 | } 9 | 10 | /// A snapshot strategy for comparing views based on pixel equality. 11 | /// 12 | /// - Parameters: 13 | /// - precision: The percentage of pixels that must match. 14 | /// - size: A view size override. 15 | public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { 16 | return SimplySnapshotting.image(precision: precision).asyncPullback { view in 17 | let initialSize = view.frame.size 18 | if let size = size { view.frame.size = size } 19 | guard view.frame.width > 0, view.frame.height > 0 else { 20 | fatalError("View not renderable to image at size \(view.frame.size)") 21 | } 22 | return view.snapshot ?? Async { callback in 23 | addImagesForRenderedViews(view).sequence().run { views in 24 | let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)! 25 | view.cacheDisplay(in: view.bounds, to: bitmapRep) 26 | let image = NSImage(size: view.bounds.size) 27 | image.addRepresentation(bitmapRep) 28 | callback(image) 29 | views.forEach { $0.removeFromSuperview() } 30 | view.frame.size = initialSize 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | extension Snapshotting where Value == NSView, Format == String { 38 | /// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies. 39 | public static var recursiveDescription: Snapshotting { 40 | return SimplySnapshotting.lines.pullback { view in 41 | return purgePointers( 42 | view.perform(Selector(("_subtreeDescription"))).retain().takeUnretainedValue() 43 | as! String 44 | ) 45 | } 46 | } 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/DataTransferTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTransferTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 25/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class DataTransferTests: XCTestCase { 12 | 13 | var dataTransferService: DataTransferService! 14 | var networkManager: MockNetworkManager! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | networkManager = MockNetworkManager() 19 | dataTransferService = DefaultDataTransferService(networkManager: networkManager) 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | networkManager = nil 25 | dataTransferService = nil 26 | } 27 | 28 | func testRequestSuccessResponse() async throws { 29 | networkManager.data = MockData.productsRawData 30 | let request = DefaultNetworkRequest(path: "/products") 31 | let productPage = try await getProductData(request: request) 32 | XCTAssertEqual(productPage.products.count, 5) 33 | } 34 | 35 | func getProductData(request: NetworkRequest) async throws -> ProductPageDataListDTO { 36 | try await dataTransferService.request(request: request) 37 | } 38 | 39 | func testRequestFailureCase() async throws { 40 | networkManager.error = NSError(domain: "Failed", code: 0) 41 | let request = DefaultNetworkRequest(path: "/products") 42 | do { 43 | _ = try await getProductData(request: request) 44 | XCTFail("Should not succeed") 45 | } catch { 46 | XCTAssertNotNil(error) 47 | } 48 | } 49 | 50 | func testDecodingFailureCase() async throws { 51 | networkManager.data = Data() 52 | let request = DefaultNetworkRequest(path: "/products") 53 | do { 54 | _ = try await getProductData(request: request) 55 | XCTFail("Should not succeed") 56 | } catch { 57 | XCTAssertEqual(error as! NetworkError, NetworkError.unableToDecode) 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Networking/URLRequestCreater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequestGenerator.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLRequestGenerator { 11 | func generateURLRequest(with config: NetworkConfigurable, from request: NetworkRequest) throws -> URLRequest 12 | } 13 | 14 | final class DefaultURLRequestGenerator: URLRequestGenerator { 15 | 16 | /// Method to create URLRequest 17 | /// - Parameters: 18 | /// - config: Network Config 19 | /// - request: Network Request 20 | /// - Returns: URLRequest 21 | func generateURLRequest(with config: NetworkConfigurable, from request: NetworkRequest) throws -> URLRequest { 22 | let url = try createURL(with: config, from: request) 23 | var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) 24 | urlRequest.httpMethod = request.method.rawValue 25 | if !request.bodyParameters.isEmpty { 26 | do { 27 | let bodyData = try JSONSerialization.data(withJSONObject: request.bodyParameters, 28 | options: [.prettyPrinted]) 29 | urlRequest.httpBody = bodyData 30 | } catch { 31 | throw error 32 | } 33 | } 34 | config.headers.forEach { key, value in 35 | urlRequest.setValue(value, forHTTPHeaderField: key) 36 | } 37 | return urlRequest 38 | } 39 | 40 | /// Method to ceate URL 41 | /// - Parameters: 42 | /// - config: Network Config 43 | /// - request: Network Request 44 | /// - Returns: URL 45 | private func createURL(with config: NetworkConfigurable, from request: NetworkRequest) throws -> URL { 46 | var components = URLComponents() 47 | components.scheme = "https" 48 | components.host = config.baseURL 49 | components.path = request.path 50 | components.queryItems = request.queryParameters.map { URLQueryItem(name: $0, value: "\($1)") } 51 | guard let url = components.url else { throw NetworkError.badURL } 52 | return url 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Utility/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // Kingfisher 4 | // 5 | // Created by onevcat on 2018/09/22. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | // These helper methods are not public since we do not want them to be exposed or cause any conflicting. 30 | // However, they are just wrapper of `ResultUtil` static methods. 31 | extension Result where Failure: Error { 32 | 33 | /// Evaluates the given transform closures to create a single output value. 34 | /// 35 | /// - Parameters: 36 | /// - onSuccess: A closure that transforms the success value. 37 | /// - onFailure: A closure that transforms the error value. 38 | /// - Returns: A single `Output` value. 39 | func match( 40 | onSuccess: (Success) -> Output, 41 | onFailure: (Failure) -> Output) -> Output 42 | { 43 | switch self { 44 | case let .success(value): 45 | return onSuccess(value) 46 | case let .failure(error): 47 | return onFailure(error) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean/Pods-ProductClean-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2019 Wei Wang 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | 40 | License 41 | MIT 42 | Title 43 | Kingfisher 44 | Type 45 | PSGroupSpecifier 46 | 47 | 48 | FooterText 49 | Generated by CocoaPods - https://cocoapods.org 50 | Title 51 | 52 | Type 53 | PSGroupSpecifier 54 | 55 | 56 | StringsTable 57 | Acknowledgements 58 | Title 59 | Acknowledgements 60 | 61 | 62 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductCleanSnapshotTests/Pods-ProductCleanSnapshotTests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | MIT License 18 | 19 | Copyright (c) 2019 Point-Free, Inc. 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | SnapshotTesting 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /ProductClean/Pods/Target Support Files/Pods-ProductClean-ProductCleanUITests/Pods-ProductClean-ProductCleanUITests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2019 Wei Wang 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | 40 | License 41 | MIT 42 | Title 43 | Kingfisher 44 | Type 45 | PSGroupSpecifier 46 | 47 | 48 | FooterText 49 | Generated by CocoaPods - https://cocoapods.org 50 | Title 51 | 52 | Type 53 | PSGroupSpecifier 54 | 55 | 56 | StringsTable 57 | Acknowledgements 58 | Title 59 | Acknowledgements 60 | 61 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/UIView.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | import UIKit 3 | 4 | extension Snapshotting where Value == UIView, Format == UIImage { 5 | /// A snapshot strategy for comparing views based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image() 8 | } 9 | 10 | /// A snapshot strategy for comparing views based on pixel equality. 11 | /// 12 | /// - Parameters: 13 | /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. 14 | /// - precision: The percentage of pixels that must match. 15 | /// - size: A view size override. 16 | /// - traits: A trait collection override. 17 | public static func image( 18 | drawHierarchyInKeyWindow: Bool = false, 19 | precision: Float = 1, 20 | size: CGSize? = nil, 21 | traits: UITraitCollection = .init() 22 | ) 23 | -> Snapshotting { 24 | 25 | return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in 26 | snapshotView( 27 | config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()), 28 | drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, 29 | traits: traits, 30 | view: view, 31 | viewController: .init() 32 | ) 33 | } 34 | } 35 | } 36 | 37 | extension Snapshotting where Value == UIView, Format == String { 38 | /// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies. 39 | public static var recursiveDescription: Snapshotting { 40 | return Snapshotting.recursiveDescription() 41 | } 42 | 43 | /// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies. 44 | public static func recursiveDescription( 45 | size: CGSize? = nil, 46 | traits: UITraitCollection = .init() 47 | ) 48 | -> Snapshotting { 49 | return SimplySnapshotting.lines.pullback { view in 50 | let dispose = prepareView( 51 | config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: traits), 52 | drawHierarchyInKeyWindow: false, 53 | traits: .init(), 54 | view: view, 55 | viewController: .init() 56 | ) 57 | defer { dispose() } 58 | return purgePointers( 59 | view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue() 60 | as! String 61 | ) 62 | } 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /ProductClean/ProductClean/Modules/Products/Presentation/ViewModel/ProductListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListViewModel.swift 3 | // ProductClean 4 | // 5 | // Created by Sajib Ghosh on 20/02/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductListViewModelProtocol: ObservableObject { 11 | var products: [ProductListItemViewModel] {get set} 12 | var isError: Bool {get} 13 | var error: String {get} 14 | var isEmpty: Bool {get} 15 | var title: String {get} 16 | func shouldShowLoader() -> Bool 17 | func fetchProducts() async 18 | } 19 | 20 | final class ProductListViewModel: ProductListViewModelProtocol { 21 | 22 | @Published var products: [ProductListItemViewModel] = [] 23 | @Published var isError: Bool = false 24 | @Published var error: String = "" 25 | var isEmpty: Bool { return products.isEmpty } 26 | var title: String = AppConstant.productListTitle 27 | private let productListUseCase: ProductListUseCase! 28 | init(useCase: ProductListUseCase) { 29 | self.productListUseCase = useCase 30 | } 31 | 32 | /// This method fetches products and catches error if any 33 | @MainActor func fetchProducts() async { 34 | do { 35 | let productList = try await productListUseCase.fetchProductList() 36 | self.products = self.transformFetchedProducts(products: productList) 37 | self.isError = false 38 | } catch { 39 | self.isError = true 40 | if let networkError = error as? NetworkError { 41 | self.error = networkError.description 42 | } else { 43 | self.error = error.localizedDescription 44 | } 45 | } 46 | } 47 | 48 | /// This method maps Product to ProductListItemViewModel 49 | /// - Parameter products:array of Product 50 | /// - Returns: array of ProductListItemViewModel 51 | private func transformFetchedProducts(products: [ProductDomainListDTO]) -> [ProductListItemViewModel] { 52 | products.map { ProductListItemViewModel(id: $0.productId, 53 | title: $0.title, 54 | description: $0.description, 55 | price: $0.price.getAmountWithCurrency(), 56 | category: $0.category.capitalized, 57 | image: $0.thumbnail) } 58 | } 59 | 60 | /// This method checks if the loader should be shown or not 61 | /// - Returns: True if there the product array is empty and error is not there 62 | func shouldShowLoader() -> Bool { 63 | return (isEmpty && !isError) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ProductClean/ProductCleanTests/Networking/NetworkManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManagerTests.swift 3 | // ProductCleanTests 4 | // 5 | // Created by Sajib Ghosh on 22/02/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ProductClean 10 | 11 | final class NetworkManagerTests: XCTestCase { 12 | 13 | var networkManger: NetworkManager! 14 | var sessionManager: MockNetworkSessionManager! 15 | 16 | var response = HTTPURLResponse(url: URL(string: "/products")!, statusCode: 200, httpVersion: nil, headerFields: nil) 17 | var invalidResponse = HTTPURLResponse(url: URL(string: "/zzzz")!, statusCode: 400, httpVersion: nil, headerFields: nil) 18 | 19 | override func setUp() { 20 | super.setUp() 21 | sessionManager = MockNetworkSessionManager() 22 | networkManger = DefaultNetworkManager(config: MockApiDataNetworkConfig(), sessionManager: sessionManager) 23 | } 24 | 25 | override func tearDown() { 26 | super.tearDown() 27 | networkManger = nil 28 | sessionManager = nil 29 | } 30 | 31 | func testRequestSuccessResponse() async throws { 32 | sessionManager.data = MockData.productsRawData 33 | sessionManager.response = response 34 | let request = MockNetworkRequest() 35 | let data = try await getProductData(request: request) 36 | XCTAssertNotNil(data) 37 | } 38 | 39 | func getProductData(request: NetworkRequest) async throws -> Data { 40 | try await networkManger.fetch(request: request) 41 | } 42 | 43 | func testRequestFailureCase() async throws { 44 | sessionManager.error = NSError(domain: "Failed", code: 0) 45 | let request = MockNetworkRequest() 46 | do { 47 | _ = try await getProductData(request: request) 48 | XCTFail("Should not succeed") 49 | } catch { 50 | XCTAssertNotNil(error) 51 | } 52 | } 53 | 54 | func testRequestFailedResponseCase() async throws { 55 | sessionManager.data = MockData.productsRawData 56 | sessionManager.response = invalidResponse 57 | let request = MockNetworkRequest() 58 | do { 59 | _ = try await getProductData(request: request) 60 | XCTFail("Should not succeed") 61 | } catch { 62 | XCTAssertEqual(error as! NetworkError, NetworkError.failed) 63 | } 64 | } 65 | 66 | func testEmptyResponseFailureCase() async throws { 67 | sessionManager.data = MockData.productsRawData 68 | let request = MockNetworkRequest() 69 | do { 70 | _ = try await getProductData(request: request) 71 | XCTFail("Should not succeed") 72 | } catch { 73 | XCTAssertEqual(error as! NetworkError, NetworkError.noResponse) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Common/String+SpecialCharacters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | /// Checks whether the string has escaped special character literals or not. 6 | /// 7 | /// This method won't detect an unescaped special character. 8 | /// For example, this method will return true for "\\n" or #"\n"#, but false for "\n" 9 | /// 10 | /// The following are the special character literals that this methods looks for: 11 | /// The escaped special characters \0 (null character), \\ (backslash), 12 | /// \t (horizontal tab), \n (line feed), \r (carriage return), 13 | /// \" (double quotation mark) and \' (single quotation mark), 14 | /// An arbitrary Unicode scalar value, written as \u{n}, 15 | /// where n is a 1–8 digit hexadecimal number (Unicode is discussed in Unicode below) 16 | /// The character sequence "# 17 | /// 18 | /// - Returns: True if the string has any special character literals, false otherwise. 19 | func hasEscapedSpecialCharactersLiteral() -> Bool { 20 | let multilineLiteralAndNumberSign = ##""" 21 | """# 22 | """## 23 | let patterns = [ 24 | // Matches \u{n} where n is a 1–8 digit hexadecimal number 25 | try? NSRegularExpression(pattern: #"\\u\{[a-fA-f0-9]{1,8}\}"#, options: .init()), 26 | try? NSRegularExpression(pattern: #"\0"#, options: .ignoreMetacharacters), 27 | try? NSRegularExpression(pattern: #"\\"#, options: .ignoreMetacharacters), 28 | try? NSRegularExpression(pattern: #"\t"#, options: .ignoreMetacharacters), 29 | try? NSRegularExpression(pattern: #"\n"#, options: .ignoreMetacharacters), 30 | try? NSRegularExpression(pattern: #"\r"#, options: .ignoreMetacharacters), 31 | try? NSRegularExpression(pattern: #"\""#, options: .ignoreMetacharacters), 32 | try? NSRegularExpression(pattern: #"\'"#, options: .ignoreMetacharacters), 33 | try? NSRegularExpression(pattern: multilineLiteralAndNumberSign, options: .ignoreMetacharacters), 34 | ] 35 | let matches = patterns.compactMap { $0?.firstMatch(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count)) } 36 | return matches.count > 0 37 | } 38 | 39 | 40 | /// This method calculates how many number signs (#) we need to add around a string 41 | /// literal to properly escape its content. 42 | /// 43 | /// Multiple # are needed when the literal contains "#, "##, "### ... 44 | /// 45 | /// - Returns: The number of "number signs(#)" needed around a string literal. 46 | /// When there is no "#, ... return 1 47 | func numberOfNumberSignsNeeded() -> Int { 48 | let pattern = try! NSRegularExpression(pattern: ##""#{1,}"##, options: .init()) 49 | 50 | let matches = pattern.matches(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count)) 51 | 52 | // If we have "## then the length of the match is 3, 53 | // which is also the number of "number signs (#)" we need to add 54 | // before and after the string literal 55 | return matches.map { $0.range.length }.max() ?? 1 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftUI) 2 | import Foundation 3 | import SwiftUI 4 | 5 | /// The size constraint for a snapshot (similar to `PreviewLayout`). 6 | public enum SwiftUISnapshotLayout { 7 | #if os(iOS) || os(tvOS) 8 | /// Center the view in a device container described by`config`. 9 | case device(config: ViewImageConfig) 10 | #endif 11 | /// Center the view in a fixed size container. 12 | case fixed(width: CGFloat, height: CGFloat) 13 | /// Fit the view to the ideal size that fits its content. 14 | case sizeThatFits 15 | } 16 | 17 | #if os(iOS) || os(tvOS) 18 | @available(iOS 13.0, tvOS 13.0, *) 19 | extension Snapshotting where Value: SwiftUI.View, Format == UIImage { 20 | 21 | /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. 22 | public static var image: Snapshotting { 23 | return .image() 24 | } 25 | 26 | /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. 27 | /// 28 | /// - Parameters: 29 | /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. 30 | /// - precision: The percentage of pixels that must match. 31 | /// - size: A view size override. 32 | /// - traits: A trait collection override. 33 | public static func image( 34 | drawHierarchyInKeyWindow: Bool = false, 35 | precision: Float = 1, 36 | layout: SwiftUISnapshotLayout = .sizeThatFits, 37 | traits: UITraitCollection = .init() 38 | ) 39 | -> Snapshotting { 40 | let config: ViewImageConfig 41 | 42 | switch layout { 43 | #if os(iOS) || os(tvOS) 44 | case let .device(config: deviceConfig): 45 | config = deviceConfig 46 | #endif 47 | case .sizeThatFits: 48 | config = .init(safeArea: .zero, size: nil, traits: traits) 49 | case let .fixed(width: width, height: height): 50 | let size = CGSize(width: width, height: height) 51 | config = .init(safeArea: .zero, size: size, traits: traits) 52 | } 53 | 54 | return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in 55 | var config = config 56 | 57 | let controller: UIViewController 58 | 59 | if config.size != nil { 60 | controller = UIHostingController.init( 61 | rootView: view 62 | ) 63 | } else { 64 | let hostingController = UIHostingController.init(rootView: view) 65 | 66 | let maxSize = CGSize(width: 0.0, height: 0.0) 67 | config.size = hostingController.sizeThatFits(in: maxSize) 68 | 69 | controller = hostingController 70 | } 71 | 72 | return snapshotView( 73 | config: config, 74 | drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, 75 | traits: traits, 76 | view: controller.view, 77 | viewController: controller 78 | ) 79 | } 80 | } 81 | } 82 | #endif 83 | #endif 84 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/URLRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | extension Snapshotting where Value == URLRequest, Format == String { 7 | /// A snapshot strategy for comparing requests based on raw equality. 8 | public static let raw = Snapshotting.raw(pretty: false) 9 | 10 | /// A snapshot strategy for comparing requests based on raw equality. 11 | /// 12 | /// - Parameter pretty: Attempts to pretty print the body of the request (supports JSON). 13 | public static func raw(pretty: Bool) -> Snapshotting { 14 | return SimplySnapshotting.lines.pullback { (request: URLRequest) in 15 | let method = "\(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "(null)")" 16 | 17 | let headers = (request.allHTTPHeaderFields ?? [:]) 18 | .map { key, value in "\(key): \(value)" } 19 | .sorted() 20 | 21 | let body: [String] 22 | do { 23 | if pretty, #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) { 24 | body = try request.httpBody 25 | .map { try JSONSerialization.jsonObject(with: $0, options: []) } 26 | .map { try JSONSerialization.data(withJSONObject: $0, options: [.prettyPrinted, .sortedKeys]) } 27 | .map { ["\n\(String(decoding: $0, as: UTF8.self))"] } 28 | ?? [] 29 | } else { 30 | throw NSError(domain: "co.pointfree.Never", code: 1, userInfo: nil) 31 | } 32 | } 33 | catch { 34 | body = request.httpBody 35 | .map { ["\n\(String(decoding: $0, as: UTF8.self))"] } 36 | ?? [] 37 | } 38 | 39 | return ([method] + headers + body).joined(separator: "\n") 40 | } 41 | } 42 | 43 | /// A snapshot strategy for comparing requests based on a cURL representation. 44 | public static let curl = SimplySnapshotting.lines.pullback { (request: URLRequest) in 45 | 46 | var components = ["curl"] 47 | 48 | // HTTP Method 49 | let httpMethod = request.httpMethod! 50 | switch httpMethod { 51 | case "GET": break 52 | case "HEAD": components.append("--head") 53 | default: components.append("--request \(httpMethod)") 54 | } 55 | 56 | // Headers 57 | if let headers = request.allHTTPHeaderFields { 58 | for field in headers.keys.sorted() where field != "Cookie" { 59 | let escapedValue = headers[field]!.replacingOccurrences(of: "\"", with: "\\\"") 60 | components.append("--header \"\(field): \(escapedValue)\"") 61 | } 62 | } 63 | 64 | // Body 65 | if let httpBodyData = request.httpBody, let httpBody = String(data: httpBodyData, encoding: .utf8) { 66 | var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") 67 | escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") 68 | 69 | components.append("--data \"\(escapedBody)\"") 70 | } 71 | 72 | // Cookies 73 | if let cookie = request.allHTTPHeaderFields?["Cookie"] { 74 | let escapedValue = cookie.replacingOccurrences(of: "\"", with: "\\\"") 75 | components.append("--cookie \"\(escapedValue)\"") 76 | } 77 | 78 | // URL 79 | components.append("\"\(request.url!.absoluteString)\"") 80 | 81 | return components.joined(separator: " \\\n\t") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ProductClean/Pods/SnapshotTesting/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | import Cocoa 3 | 4 | extension Snapshotting where Value == NSBezierPath, Format == NSImage { 5 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 6 | public static var image: Snapshotting { 7 | return .image() 8 | } 9 | 10 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 11 | /// 12 | /// - Parameter precision: The percentage of pixels that must match. 13 | public static func image(precision: Float = 1) -> Snapshotting { 14 | return SimplySnapshotting.image(precision: precision).pullback { path in 15 | // Move path info frame: 16 | let bounds = path.bounds 17 | let transform = AffineTransform(translationByX: -bounds.origin.x, byY: -bounds.origin.y) 18 | path.transform(using: transform) 19 | 20 | let image = NSImage(size: path.bounds.size) 21 | image.lockFocus() 22 | path.fill() 23 | image.unlockFocus() 24 | return image 25 | } 26 | } 27 | } 28 | 29 | extension Snapshotting where Value == NSBezierPath, Format == String { 30 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 31 | @available(iOS 11.0, *) 32 | public static var elementsDescription: Snapshotting { 33 | return .elementsDescription(numberFormatter: defaultNumberFormatter) 34 | } 35 | 36 | /// A snapshot strategy for comparing bezier paths based on pixel equality. 37 | /// 38 | /// - Parameter numberFormatter: The number formatter used for formatting points. 39 | @available(iOS 11.0, *) 40 | public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting { 41 | let namesByType: [NSBezierPath.ElementType: String] = [ 42 | .moveTo: "MoveTo", 43 | .lineTo: "LineTo", 44 | .curveTo: "CurveTo", 45 | .closePath: "Close", 46 | ] 47 | 48 | let numberOfPointsByType: [NSBezierPath.ElementType: Int] = [ 49 | .moveTo: 1, 50 | .lineTo: 1, 51 | .curveTo: 3, 52 | .closePath: 0, 53 | ] 54 | 55 | return SimplySnapshotting.lines.pullback { path in 56 | var string: String = "" 57 | 58 | var elementPoints = [CGPoint](repeating: .zero, count: 3) 59 | for elementIndex in 0.. 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | private let sharedProcessingQueue: CallbackQueue = 30 | .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) 31 | 32 | // Handles image processing work on an own process queue. 33 | class ImageDataProcessor { 34 | let data: Data 35 | let callbacks: [SessionDataTask.TaskCallback] 36 | let queue: CallbackQueue 37 | 38 | // Note: We have an optimization choice there, to reduce queue dispatch by checking callback 39 | // queue settings in each option... 40 | let onImageProcessed = Delegate<(Result, SessionDataTask.TaskCallback), Void>() 41 | 42 | init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { 43 | self.data = data 44 | self.callbacks = callbacks 45 | self.queue = processingQueue ?? sharedProcessingQueue 46 | } 47 | 48 | func process() { 49 | queue.execute(doProcess) 50 | } 51 | 52 | private func doProcess() { 53 | var processedImages = [String: KFCrossPlatformImage]() 54 | for callback in callbacks { 55 | let processor = callback.options.processor 56 | var image = processedImages[processor.identifier] 57 | if image == nil { 58 | image = processor.process(item: .data(data), options: callback.options) 59 | processedImages[processor.identifier] = image 60 | } 61 | 62 | let result: Result 63 | if let image = image { 64 | let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image 65 | result = .success(finalImage) 66 | } else { 67 | let error = KingfisherError.processorError( 68 | reason: .processingFailed(processor: processor, item: .data(data))) 69 | result = .failure(error) 70 | } 71 | onImageProcessed.call((result, callback)) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Image/GraphicsContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphicsContext.swift 3 | // Kingfisher 4 | // 5 | // Created by taras on 19/04/2021. 6 | // 7 | // Copyright (c) 2021 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 28 | import AppKit 29 | #endif 30 | #if canImport(UIKit) 31 | import UIKit 32 | #endif 33 | 34 | enum GraphicsContext { 35 | static func begin(size: CGSize, scale: CGFloat) { 36 | #if os(macOS) 37 | NSGraphicsContext.saveGraphicsState() 38 | #else 39 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 40 | #endif 41 | } 42 | 43 | static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? { 44 | #if os(macOS) 45 | guard let rep = NSBitmapImageRep( 46 | bitmapDataPlanes: nil, 47 | pixelsWide: Int(size.width), 48 | pixelsHigh: Int(size.height), 49 | bitsPerSample: cgImage?.bitsPerComponent ?? 8, 50 | samplesPerPixel: 4, 51 | hasAlpha: true, 52 | isPlanar: false, 53 | colorSpaceName: .calibratedRGB, 54 | bytesPerRow: 0, 55 | bitsPerPixel: 0) else 56 | { 57 | assertionFailure("[Kingfisher] Image representation cannot be created.") 58 | return nil 59 | } 60 | rep.size = size 61 | guard let context = NSGraphicsContext(bitmapImageRep: rep) else { 62 | assertionFailure("[Kingfisher] Image context cannot be created.") 63 | return nil 64 | } 65 | 66 | NSGraphicsContext.current = context 67 | return context.cgContext 68 | #else 69 | guard let context = UIGraphicsGetCurrentContext() else { 70 | return nil 71 | } 72 | if inverting { // If drawing a CGImage, we need to make context flipped. 73 | context.scaleBy(x: 1.0, y: -1.0) 74 | context.translateBy(x: 0, y: -size.height) 75 | } 76 | return context 77 | #endif 78 | } 79 | 80 | static func end() { 81 | #if os(macOS) 82 | NSGraphicsContext.restoreGraphicsState() 83 | #else 84 | UIGraphicsEndImageContext() 85 | #endif 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallbackQueue.swift 3 | // Kingfisher 4 | // 5 | // Created by onevcat on 2018/10/15. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | public typealias ExecutionQueue = CallbackQueue 30 | 31 | /// Represents callback queue behaviors when an calling of closure be dispatched. 32 | /// 33 | /// - asyncMain: Dispatch the calling to `DispatchQueue.main` with an `async` behavior. 34 | /// - currentMainOrAsync: Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not 35 | /// `.main`. Otherwise, call the closure immediately in current main queue. 36 | /// - untouch: Do not change the calling queue for closure. 37 | /// - dispatch: Dispatches to a specified `DispatchQueue`. 38 | public enum CallbackQueue { 39 | /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior. 40 | case mainAsync 41 | /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not 42 | /// `.main`. Otherwise, call the closure immediately in current main queue. 43 | case mainCurrentOrAsync 44 | /// Do not change the calling queue for closure. 45 | case untouch 46 | /// Dispatches to a specified `DispatchQueue`. 47 | case dispatch(DispatchQueue) 48 | 49 | public func execute(_ block: @escaping () -> Void) { 50 | switch self { 51 | case .mainAsync: 52 | DispatchQueue.main.async { block() } 53 | case .mainCurrentOrAsync: 54 | DispatchQueue.main.safeAsync { block() } 55 | case .untouch: 56 | block() 57 | case .dispatch(let queue): 58 | queue.async { block() } 59 | } 60 | } 61 | 62 | var queue: DispatchQueue { 63 | switch self { 64 | case .mainAsync: return .main 65 | case .mainCurrentOrAsync: return .main 66 | case .untouch: return OperationQueue.current?.underlyingQueue ?? .main 67 | case .dispatch(let queue): return queue 68 | } 69 | } 70 | } 71 | 72 | extension DispatchQueue { 73 | // This method will dispatch the `block` to self. 74 | // If `self` is the main queue, and current thread is main thread, the block 75 | // will be invoked immediately instead of being dispatched. 76 | func safeAsync(_ block: @escaping () -> Void) { 77 | if self === DispatchQueue.main && Thread.isMainThread { 78 | block() 79 | } else { 80 | async { block() } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Image/Placeholder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Placeholder.swift 3 | // Kingfisher 4 | // 5 | // Created by Tieme van Veen on 28/08/2017. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if !os(watchOS) 28 | 29 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 30 | import AppKit 31 | #endif 32 | 33 | #if canImport(UIKit) 34 | import UIKit 35 | #endif 36 | 37 | /// Represents a placeholder type which could be set while loading as well as 38 | /// loading finished without getting an image. 39 | public protocol Placeholder { 40 | 41 | /// How the placeholder should be added to a given image view. 42 | func add(to imageView: KFCrossPlatformImageView) 43 | 44 | /// How the placeholder should be removed from a given image view. 45 | func remove(from imageView: KFCrossPlatformImageView) 46 | } 47 | 48 | /// Default implementation of an image placeholder. The image will be set or 49 | /// reset directly for `image` property of the image view. 50 | extension KFCrossPlatformImage: Placeholder { 51 | /// How the placeholder should be added to a given image view. 52 | public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self } 53 | 54 | /// How the placeholder should be removed from a given image view. 55 | public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil } 56 | } 57 | 58 | /// Default implementation of an arbitrary view as placeholder. The view will be 59 | /// added as a subview when adding and be removed from its super view when removing. 60 | /// 61 | /// To use your customize View type as placeholder, simply let it conforming to 62 | /// `Placeholder` by `extension MyView: Placeholder {}`. 63 | extension Placeholder where Self: KFCrossPlatformView { 64 | 65 | /// How the placeholder should be added to a given image view. 66 | public func add(to imageView: KFCrossPlatformImageView) { 67 | imageView.addSubview(self) 68 | translatesAutoresizingMaskIntoConstraints = false 69 | 70 | centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true 71 | centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true 72 | heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true 73 | widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true 74 | } 75 | 76 | /// How the placeholder should be removed from a given image view. 77 | public func remove(from imageView: KFCrossPlatformImageView) { 78 | removeFromSuperview() 79 | } 80 | } 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedirectHandler.swift 3 | // Kingfisher 4 | // 5 | // Created by Roman Maidanovych on 2018/12/10. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// Represents and wraps a method for modifying request during an image download request redirection. 30 | public protocol ImageDownloadRedirectHandler { 31 | 32 | /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. 33 | /// This is the posibility you can modify the image download request during redirection. You can modify the 34 | /// request for some customizing purpose, such as adding auth token to the header, do basic HTTP auth or 35 | /// something like url mapping. 36 | /// 37 | /// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of 38 | /// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods. 39 | /// 40 | /// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it. 41 | /// 42 | /// - Parameters: 43 | /// - task: The current `SessionDataTask` which triggers this redirect. 44 | /// - response: The response received during redirection. 45 | /// - newRequest: The request for redirection which can be modified. 46 | /// - completionHandler: A closure for being called with modified request. 47 | func handleHTTPRedirection( 48 | for task: SessionDataTask, 49 | response: HTTPURLResponse, 50 | newRequest: URLRequest, 51 | completionHandler: @escaping (URLRequest?) -> Void) 52 | } 53 | 54 | /// A wrapper for creating an `ImageDownloadRedirectHandler` easier. 55 | /// This type conforms to `ImageDownloadRedirectHandler` and wraps a redirect request modify block. 56 | public struct AnyRedirectHandler: ImageDownloadRedirectHandler { 57 | 58 | let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void 59 | 60 | public func handleHTTPRedirection( 61 | for task: SessionDataTask, 62 | response: HTTPURLResponse, 63 | newRequest: URLRequest, 64 | completionHandler: @escaping (URLRequest?) -> Void) 65 | { 66 | block(task, response, newRequest, completionHandler) 67 | } 68 | 69 | /// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block. 70 | /// 71 | /// - Parameter modify: The request modifying block runs when a request modifying task comes. 72 | /// 73 | public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) { 74 | block = handle 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageContext.swift 3 | // Kingfisher 4 | // 5 | // Created by onevcat on 2021/05/08. 6 | // 7 | // Copyright (c) 2021 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if canImport(SwiftUI) && canImport(Combine) 28 | import SwiftUI 29 | import Combine 30 | 31 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 32 | extension KFImage { 33 | public class Context { 34 | let source: Source? 35 | var options = KingfisherParsedOptionsInfo( 36 | KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] 37 | ) 38 | 39 | var configurations: [(HoldingView) -> HoldingView] = [] 40 | var renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] 41 | var contentConfiguration: ((HoldingView) -> AnyView)? = nil 42 | 43 | var cancelOnDisappear: Bool = false 44 | var placeholder: ((Progress) -> AnyView)? = nil 45 | 46 | let onFailureDelegate = Delegate() 47 | let onSuccessDelegate = Delegate() 48 | let onProgressDelegate = Delegate<(Int64, Int64), Void>() 49 | 50 | var startLoadingBeforeViewAppear: Bool = false 51 | 52 | init(source: Source?) { 53 | self.source = source 54 | } 55 | 56 | func shouldApplyFade(cacheType: CacheType) -> Bool { 57 | options.forceTransition || cacheType == .none 58 | } 59 | 60 | func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { 61 | shouldApplyFade(cacheType: cacheType) 62 | ? options.transition.fadeDuration 63 | : nil 64 | } 65 | } 66 | } 67 | 68 | extension ImageTransition { 69 | // Only for fade effect in SwiftUI. 70 | fileprivate var fadeDuration: TimeInterval? { 71 | switch self { 72 | case .fade(let duration): 73 | return duration 74 | default: 75 | return nil 76 | } 77 | } 78 | } 79 | 80 | 81 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 82 | extension KFImage.Context: Hashable { 83 | public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { 84 | lhs.source == rhs.source && 85 | lhs.options.processor.identifier == rhs.options.processor.identifier 86 | } 87 | 88 | public func hash(into hasher: inout Hasher) { 89 | hasher.combine(source) 90 | hasher.combine(options.processor.identifier) 91 | } 92 | } 93 | 94 | #if !os(watchOS) 95 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 96 | extension KFAnimatedImage { 97 | public typealias Context = KFImage.Context 98 | typealias ImageBinder = KFImage.ImageBinder 99 | } 100 | #endif 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/General/Kingfisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kingfisher.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 16/9/14. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import ImageIO 29 | 30 | #if os(macOS) 31 | import AppKit 32 | public typealias KFCrossPlatformImage = NSImage 33 | public typealias KFCrossPlatformView = NSView 34 | public typealias KFCrossPlatformColor = NSColor 35 | public typealias KFCrossPlatformImageView = NSImageView 36 | public typealias KFCrossPlatformButton = NSButton 37 | #else 38 | import UIKit 39 | public typealias KFCrossPlatformImage = UIImage 40 | public typealias KFCrossPlatformColor = UIColor 41 | #if !os(watchOS) 42 | public typealias KFCrossPlatformImageView = UIImageView 43 | public typealias KFCrossPlatformView = UIView 44 | public typealias KFCrossPlatformButton = UIButton 45 | #if canImport(TVUIKit) 46 | import TVUIKit 47 | #endif 48 | #if canImport(CarPlay) && !targetEnvironment(macCatalyst) 49 | import CarPlay 50 | #endif 51 | #else 52 | import WatchKit 53 | #endif 54 | #endif 55 | 56 | /// Wrapper for Kingfisher compatible types. This type provides an extension point for 57 | /// convenience methods in Kingfisher. 58 | public struct KingfisherWrapper { 59 | public let base: Base 60 | public init(_ base: Base) { 61 | self.base = base 62 | } 63 | } 64 | 65 | /// Represents an object type that is compatible with Kingfisher. You can use `kf` property to get a 66 | /// value in the namespace of Kingfisher. 67 | public protocol KingfisherCompatible: AnyObject { } 68 | 69 | /// Represents a value type that is compatible with Kingfisher. You can use `kf` property to get a 70 | /// value in the namespace of Kingfisher. 71 | public protocol KingfisherCompatibleValue {} 72 | 73 | extension KingfisherCompatible { 74 | /// Gets a namespace holder for Kingfisher compatible types. 75 | public var kf: KingfisherWrapper { 76 | get { return KingfisherWrapper(self) } 77 | set { } 78 | } 79 | } 80 | 81 | extension KingfisherCompatibleValue { 82 | /// Gets a namespace holder for Kingfisher compatible types. 83 | public var kf: KingfisherWrapper { 84 | get { return KingfisherWrapper(self) } 85 | set { } 86 | } 87 | } 88 | 89 | extension KFCrossPlatformImage: KingfisherCompatible { } 90 | #if !os(watchOS) 91 | extension KFCrossPlatformImageView: KingfisherCompatible { } 92 | extension KFCrossPlatformButton: KingfisherCompatible { } 93 | extension NSTextAttachment: KingfisherCompatible { } 94 | #else 95 | extension WKInterfaceImage: KingfisherCompatible { } 96 | #endif 97 | 98 | #if os(tvOS) && canImport(TVUIKit) 99 | @available(tvOS 12.0, *) 100 | extension TVMonogramView: KingfisherCompatible { } 101 | #endif 102 | 103 | #if canImport(CarPlay) && !targetEnvironment(macCatalyst) 104 | @available(iOS 14.0, *) 105 | extension CPListItem: KingfisherCompatible { } 106 | #endif 107 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Utility/Delegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delegate.swift 3 | // Kingfisher 4 | // 5 | // Created by onevcat on 2018/10/10. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | /// A class that keeps a weakly reference for `self` when implementing `onXXX` behaviors. 29 | /// Instead of remembering to keep `self` as weak in a stored closure: 30 | /// 31 | /// ```swift 32 | /// // MyClass.swift 33 | /// var onDone: (() -> Void)? 34 | /// func done() { 35 | /// onDone?() 36 | /// } 37 | /// 38 | /// // ViewController.swift 39 | /// var obj: MyClass? 40 | /// 41 | /// func doSomething() { 42 | /// obj = MyClass() 43 | /// obj!.onDone = { [weak self] in 44 | /// self?.reportDone() 45 | /// } 46 | /// } 47 | /// ``` 48 | /// 49 | /// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside: 50 | /// 51 | /// ```swift 52 | /// // MyClass.swift 53 | /// let onDone = Delegate<(), Void>() 54 | /// func done() { 55 | /// onDone.call() 56 | /// } 57 | /// 58 | /// // ViewController.swift 59 | /// var obj: MyClass? 60 | /// 61 | /// func doSomething() { 62 | /// obj = MyClass() 63 | /// obj!.onDone.delegate(on: self) { (self, _) 64 | /// // `self` here is shadowed and does not keep a strong ref. 65 | /// // So you can release both `MyClass` instance and `ViewController` instance. 66 | /// self.reportDone() 67 | /// } 68 | /// } 69 | /// ``` 70 | /// 71 | public class Delegate { 72 | public init() {} 73 | 74 | private var block: ((Input) -> Output?)? 75 | public func delegate(on target: T, block: ((T, Input) -> Output)?) { 76 | self.block = { [weak target] input in 77 | guard let target = target else { return nil } 78 | return block?(target, input) 79 | } 80 | } 81 | 82 | public func call(_ input: Input) -> Output? { 83 | return block?(input) 84 | } 85 | 86 | public func callAsFunction(_ input: Input) -> Output? { 87 | return call(input) 88 | } 89 | } 90 | 91 | extension Delegate where Input == Void { 92 | public func call() -> Output? { 93 | return call(()) 94 | } 95 | 96 | public func callAsFunction() -> Output? { 97 | return call() 98 | } 99 | } 100 | 101 | extension Delegate where Input == Void, Output: OptionalProtocol { 102 | public func call() -> Output { 103 | return call(()) 104 | } 105 | 106 | public func callAsFunction() -> Output { 107 | return call() 108 | } 109 | } 110 | 111 | extension Delegate where Output: OptionalProtocol { 112 | public func call(_ input: Input) -> Output { 113 | if let result = block?(input) { 114 | return result 115 | } else { 116 | return Output._createNil 117 | } 118 | } 119 | 120 | public func callAsFunction(_ input: Input) -> Output { 121 | return call(input) 122 | } 123 | } 124 | 125 | public protocol OptionalProtocol { 126 | static var _createNil: Self { get } 127 | } 128 | extension Optional : OptionalProtocol { 129 | public static var _createNil: Optional { 130 | return nil 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KFImage.swift 3 | // Kingfisher 4 | // 5 | // Created by onevcat on 2019/06/26. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if canImport(SwiftUI) && canImport(Combine) 28 | import SwiftUI 29 | import Combine 30 | 31 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 32 | public struct KFImage: KFImageProtocol { 33 | public var context: Context 34 | public init(context: Context) { 35 | self.context = context 36 | } 37 | } 38 | 39 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 40 | extension Image: KFImageHoldingView { 41 | public typealias RenderingView = Image 42 | public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { 43 | Image(crossPlatformImage: image) 44 | } 45 | } 46 | 47 | // MARK: - Image compatibility. 48 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 49 | extension KFImage { 50 | 51 | public func resizable( 52 | capInsets: EdgeInsets = EdgeInsets(), 53 | resizingMode: Image.ResizingMode = .stretch) -> KFImage 54 | { 55 | configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } 56 | } 57 | 58 | public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { 59 | configure { $0.renderingMode(renderingMode) } 60 | } 61 | 62 | public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { 63 | configure { $0.interpolation(interpolation) } 64 | } 65 | 66 | public func antialiased(_ isAntialiased: Bool) -> KFImage { 67 | configure { $0.antialiased(isAntialiased) } 68 | } 69 | 70 | /// Starts the loading process of `self` immediately. 71 | /// 72 | /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading 73 | /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a 74 | /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once 75 | /// could help avoiding the flickering, with some performance trade-off. 76 | /// 77 | /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. 78 | /// It does nothing now and please just remove it. 79 | /// 80 | /// - Returns: The `Self` value with changes applied. 81 | @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") 82 | public func loadImmediately(_ start: Bool = true) -> KFImage { 83 | return self 84 | } 85 | } 86 | 87 | #if DEBUG 88 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 89 | struct KFImage_Previews: PreviewProvider { 90 | static var previews: some View { 91 | Group { 92 | KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) 93 | .onSuccess { r in 94 | print(r) 95 | } 96 | .placeholder { p in 97 | ProgressView(p) 98 | } 99 | .resizable() 100 | .aspectRatio(contentMode: .fit) 101 | .padding() 102 | } 103 | } 104 | } 105 | #endif 106 | #endif 107 | -------------------------------------------------------------------------------- /ProductClean/Pods/Kingfisher/Sources/Cache/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storage.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2018/10/15. 6 | // 7 | // Copyright (c) 2019 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// Constants for some time intervals 30 | struct TimeConstants { 31 | static let secondsInOneDay = 86_400 32 | } 33 | 34 | /// Represents the expiration strategy used in storage. 35 | /// 36 | /// - never: The item never expires. 37 | /// - seconds: The item expires after a time duration of given seconds from now. 38 | /// - days: The item expires after a time duration of given days from now. 39 | /// - date: The item expires after a given date. 40 | public enum StorageExpiration { 41 | /// The item never expires. 42 | case never 43 | /// The item expires after a time duration of given seconds from now. 44 | case seconds(TimeInterval) 45 | /// The item expires after a time duration of given days from now. 46 | case days(Int) 47 | /// The item expires after a given date. 48 | case date(Date) 49 | /// Indicates the item is already expired. Use this to skip cache. 50 | case expired 51 | 52 | func estimatedExpirationSince(_ date: Date) -> Date { 53 | switch self { 54 | case .never: return .distantFuture 55 | case .seconds(let seconds): 56 | return date.addingTimeInterval(seconds) 57 | case .days(let days): 58 | let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) 59 | return date.addingTimeInterval(duration) 60 | case .date(let ref): 61 | return ref 62 | case .expired: 63 | return .distantPast 64 | } 65 | } 66 | 67 | var estimatedExpirationSinceNow: Date { 68 | return estimatedExpirationSince(Date()) 69 | } 70 | 71 | var isExpired: Bool { 72 | return timeInterval <= 0 73 | } 74 | 75 | var timeInterval: TimeInterval { 76 | switch self { 77 | case .never: return .infinity 78 | case .seconds(let seconds): return seconds 79 | case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) 80 | case .date(let ref): return ref.timeIntervalSinceNow 81 | case .expired: return -(.infinity) 82 | } 83 | } 84 | } 85 | 86 | /// Represents the expiration extending strategy used in storage to after access. 87 | /// 88 | /// - none: The item expires after the original time, without extending after access. 89 | /// - cacheTime: The item expiration extends by the original cache time after each access. 90 | /// - expirationTime: The item expiration extends by the provided time after each access. 91 | public enum ExpirationExtending { 92 | /// The item expires after the original time, without extending after access. 93 | case none 94 | /// The item expiration extends by the original cache time after each access. 95 | case cacheTime 96 | /// The item expiration extends by the provided time after each access. 97 | case expirationTime(_ expiration: StorageExpiration) 98 | } 99 | 100 | /// Represents types which cost in memory can be calculated. 101 | public protocol CacheCostCalculable { 102 | var cacheCost: Int { get } 103 | } 104 | 105 | /// Represents types which can be converted to and from data. 106 | public protocol DataTransformable { 107 | func toData() throws -> Data 108 | static func fromData(_ data: Data) throws -> Self 109 | static var empty: Self { get } 110 | } 111 | --------------------------------------------------------------------------------