├── .gitattributes ├── .gitignore ├── README.md ├── SimpleVIPExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SimpleVIPExample.xcscheme └── SimpleVIPExample ├── AppDelegate ├── AppDelegate.swift └── SceneDelegate.swift ├── Base.lproj └── LaunchScreen.storyboard ├── Constants └── Constants.swift ├── Extension └── UIImageView+Extension.swift ├── Helper ├── APIManager.swift └── CustomErrors.swift ├── Info.plist ├── Models └── Product.swift ├── Resource └── Assets.xcassets │ ├── AccentColor.colorset │ └── Contents.json │ ├── AppIcon.appiconset │ └── Contents.json │ └── Contents.json ├── Scene ├── ProductDetails │ ├── ProductDetailsConfigurator.swift │ ├── ProductDetailsInteractor.swift │ ├── ProductDetailsInterfaces.swift │ ├── ProductDetailsPresenter.swift │ └── ProductDetailsViewController.swift └── ProductsScene │ ├── ProductsConfigurator.swift │ ├── ProductsDataSource.swift │ ├── ProductsInteractor.swift │ ├── ProductsInterfaces.swift │ ├── ProductsPresenter.swift │ ├── ProductsRouter.swift │ ├── ProductsViewController.swift │ ├── ProductsWorker.swift │ └── View │ └── ProductTableViewCell.swift └── Utility ├── EndPoint.swift └── ProductEndPoint.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | # Ignore Mac DS_Store files 86 | .DS_Store 87 | **/.DS_Store 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean VIP 2 | 3 | An Example of Clean VIP Architecture. 4 | 5 | We will discuss Uncle Bob’s Clean architecture here followed by a sample project. As we have seen, MVC architecture turned into massive view controllers with respect to time to a time change requests that lead us to difficulties from a testing perspective for each module. 6 | 7 | Below are the components of this Architecture : 8 | 9 | * View Controller 10 | * Interactor 11 | * Presenter 12 | * Worker 13 | * Router 14 | * Models 15 | 16 | ## Flow chart 17 | 18 | ![Untitled Diagram drawio(2)](https://user-images.githubusercontent.com/97470591/211023011-30890c67-7ed4-48ba-8e99-df0566dbf6cd.png) 19 | 20 | 21 | ## View Controller 22 | 23 | * Defines a scene and contains a view or views. 24 | 25 | * Keeps instances of the interactor and router. 26 | 27 | * Passes the actions from views to the interactor (output) and takes the presenter actions as input 28 | 29 | ## Interactor 30 | 31 | * Contains a Scene’s business logic. 32 | 33 | * Keeps a reference to the presenter. 34 | 35 | * Runs actions on workers based on input (from the View Controller), triggers and passes the output to the presenter. 36 | 37 | * The interactor should never import the UIKit. 38 | 39 | ## Presenter 40 | 41 | * Keeps a weak reference to the view controller that is an output of the presenter. 42 | 43 | * After the interactor produces some results, it passes the response to the presenter. Next, the presenter marshals the response into view models suitable for display and then passes the view models back to the view controller for display to the user. 44 | 45 | ## Worker 46 | 47 | * The Worker component will handle all the API/CoreData requests and responses. The Response will get the data ready for the Interactor. It will handle the success/error response, so the Interactor would know how to proceed. 48 | 49 | * Should follow the Single Responsibility principle (an interactor may contain many workers with different responsibilities). 50 | 51 | ## Router 52 | 53 | * Extracts this navigation logic out of the view controller. 54 | 55 | * Keeps a weak reference to the source (View Controller). 56 | 57 | ## Configurator 58 | 59 | * Takes the responsibility of configuring the VIP cycle by encapsulating the creation of all instances and assigning them where needed. 60 | 61 | ## Model 62 | 63 | * Decoupled data abstractions. 64 | 65 | ## When and why We should use it 66 | 67 | * Projects where unit testing is expected. 68 | 69 | * Long-term and big projects. 70 | 71 | * Projects with a generous amount of logic. 72 | 73 | * Projects you want to reuse in the future. 74 | 75 | ## Strengths 76 | 77 | * Unidirectional flow of data 78 | 79 | * Testability 80 | 81 | * Easy to maintain and fix bugs. 82 | 83 | * Enforces modularity to write shorter methods with a single responsibility. 84 | 85 | * Nice for decoupling class dependencies with established boundaries. 86 | 87 | * Extracts business logic from view controllers into interactors. 88 | 89 | * Applies to existing projects of any size. 90 | 91 | * Modular: Interfaces may be easy to change without changing the rest of the system due to using protocol conformant business logic 92 | 93 | ## Disadvantages 94 | 95 | * Clean Swift VIP architecture can be quite complex and may require a significant amount of code to implement, which can be difficult for new developers to understand and maintain. 96 | 97 | * It can be difficult to understand the flow of the code and how different components interact with each other, especially for developers who are not familiar with the Clean Swift architecture. 98 | 99 | 100 | ## Official implementation Link 101 | 102 | https://clean-swift.com/clean-swift-ios-architecture/ 103 | 104 | 105 | -------------------------------------------------------------------------------- /SimpleVIPExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6E05E3D9291C2AD700B08F4B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E05E3D8291C2AD700B08F4B /* AppDelegate.swift */; }; 11 | 6E05E3DB291C2AD700B08F4B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E05E3DA291C2AD700B08F4B /* SceneDelegate.swift */; }; 12 | 6E05E3E2291C2AD800B08F4B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E05E3E1291C2AD800B08F4B /* Assets.xcassets */; }; 13 | 6E05E3E5291C2AD800B08F4B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E05E3E3291C2AD800B08F4B /* LaunchScreen.storyboard */; }; 14 | 6E52293D29981DE900836594 /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52293C29981DE900836594 /* EndPoint.swift */; }; 15 | 6E52293F2998205C00836594 /* ProductEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52293E2998205C00836594 /* ProductEndPoint.swift */; }; 16 | 6E5229422998252900836594 /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5229412998252900836594 /* APIManager.swift */; }; 17 | 6E5229452998284900836594 /* CustomErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5229442998284900836594 /* CustomErrors.swift */; }; 18 | 6E5229472998323A00836594 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5229462998323A00836594 /* Product.swift */; }; 19 | 6E78EBF52964203E0046C6F8 /* ProductDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E78EBF42964203E0046C6F8 /* ProductDetailsViewController.swift */; }; 20 | 6E78EBF7296425E00046C6F8 /* ProductDetailsConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E78EBF6296425E00046C6F8 /* ProductDetailsConfigurator.swift */; }; 21 | 6E78EBF9296428710046C6F8 /* ProductDetailsInterfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E78EBF8296428710046C6F8 /* ProductDetailsInterfaces.swift */; }; 22 | 6E78EBFB296429910046C6F8 /* ProductDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E78EBFA296429910046C6F8 /* ProductDetailsInteractor.swift */; }; 23 | 6E78EBFD29642A140046C6F8 /* ProductDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E78EBFC29642A140046C6F8 /* ProductDetailsPresenter.swift */; }; 24 | 6E811D562999030F00214B50 /* ProductsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D552999030F00214B50 /* ProductsViewController.swift */; }; 25 | 6E811D58299904D200214B50 /* ProductsInterfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D57299904D200214B50 /* ProductsInterfaces.swift */; }; 26 | 6E811D5A2999067B00214B50 /* ProductsConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D592999067B00214B50 /* ProductsConfigurator.swift */; }; 27 | 6E811D5C29990CB000214B50 /* ProductsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D5B29990CB000214B50 /* ProductsInteractor.swift */; }; 28 | 6E811D5E2999105700214B50 /* ProductsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D5D2999105700214B50 /* ProductsPresenter.swift */; }; 29 | 6E811D60299911F800214B50 /* ProductsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D5F299911F800214B50 /* ProductsDataSource.swift */; }; 30 | 6E811D622999131B00214B50 /* ProductsWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D612999131B00214B50 /* ProductsWorker.swift */; }; 31 | 6E811D642999137700214B50 /* ProductsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D632999137700214B50 /* ProductsRouter.swift */; }; 32 | 6E811D6629992AB400214B50 /* ProductTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D6529992AB400214B50 /* ProductTableViewCell.swift */; }; 33 | 6E811D69299940B700214B50 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 6E811D68299940B700214B50 /* Kingfisher */; }; 34 | 6E811D6C2999432800214B50 /* UIImageView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E811D6B2999432800214B50 /* UIImageView+Extension.swift */; }; 35 | 6EF2B6322962F9370041242B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF2B6312962F9370041242B /* Constants.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXContainerItemProxy section */ 39 | 6EA5C4ED2965CB53008D1927 /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = 6E05E3CD291C2AD700B08F4B /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = 6E05E3D4291C2AD700B08F4B; 44 | remoteInfo = SimpleVIPExample; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 6E05E3D5291C2AD700B08F4B /* SimpleVIPExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleVIPExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 6E05E3D8291C2AD700B08F4B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 51 | 6E05E3DA291C2AD700B08F4B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 52 | 6E05E3E1291C2AD800B08F4B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | 6E05E3E4291C2AD800B08F4B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | 6E05E3E6291C2AD800B08F4B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 6E52293C29981DE900836594 /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; 56 | 6E52293E2998205C00836594 /* ProductEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductEndPoint.swift; sourceTree = ""; }; 57 | 6E5229412998252900836594 /* APIManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = ""; }; 58 | 6E5229442998284900836594 /* CustomErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomErrors.swift; sourceTree = ""; }; 59 | 6E5229462998323A00836594 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; 60 | 6E78EBF42964203E0046C6F8 /* ProductDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsViewController.swift; sourceTree = ""; }; 61 | 6E78EBF6296425E00046C6F8 /* ProductDetailsConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsConfigurator.swift; sourceTree = ""; }; 62 | 6E78EBF8296428710046C6F8 /* ProductDetailsInterfaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsInterfaces.swift; sourceTree = ""; }; 63 | 6E78EBFA296429910046C6F8 /* ProductDetailsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsInteractor.swift; sourceTree = ""; }; 64 | 6E78EBFC29642A140046C6F8 /* ProductDetailsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsPresenter.swift; sourceTree = ""; }; 65 | 6E811D552999030F00214B50 /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = ""; }; 66 | 6E811D57299904D200214B50 /* ProductsInterfaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsInterfaces.swift; sourceTree = ""; }; 67 | 6E811D592999067B00214B50 /* ProductsConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsConfigurator.swift; sourceTree = ""; }; 68 | 6E811D5B29990CB000214B50 /* ProductsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsInteractor.swift; sourceTree = ""; }; 69 | 6E811D5D2999105700214B50 /* ProductsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsPresenter.swift; sourceTree = ""; }; 70 | 6E811D5F299911F800214B50 /* ProductsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsDataSource.swift; sourceTree = ""; }; 71 | 6E811D612999131B00214B50 /* ProductsWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsWorker.swift; sourceTree = ""; }; 72 | 6E811D632999137700214B50 /* ProductsRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRouter.swift; sourceTree = ""; }; 73 | 6E811D6529992AB400214B50 /* ProductTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductTableViewCell.swift; sourceTree = ""; }; 74 | 6E811D6B2999432800214B50 /* UIImageView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extension.swift"; sourceTree = ""; }; 75 | 6EA5C4E92965CB53008D1927 /* SimpleVIPExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleVIPExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 6EF2B6312962F9370041242B /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 6E05E3D2291C2AD700B08F4B /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 6E811D69299940B700214B50 /* Kingfisher in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | 6EA5C4E62965CB53008D1927 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXFrameworksBuildPhase section */ 96 | 97 | /* Begin PBXGroup section */ 98 | 6E05E3CC291C2AD700B08F4B = { 99 | isa = PBXGroup; 100 | children = ( 101 | 6E05E3D7291C2AD700B08F4B /* SimpleVIPExample */, 102 | 6E05E3D6291C2AD700B08F4B /* Products */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 6E05E3D6291C2AD700B08F4B /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 6E05E3D5291C2AD700B08F4B /* SimpleVIPExample.app */, 110 | 6EA5C4E92965CB53008D1927 /* SimpleVIPExampleTests.xctest */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | 6E05E3D7291C2AD700B08F4B /* SimpleVIPExample */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 6E05E3EC291C2E2100B08F4B /* AppDelegate */, 119 | 6EF2B61A2962D7420041242B /* Resource */, 120 | 6E5229432998255200836594 /* Constants */, 121 | 6E811D6A2999430A00214B50 /* Extension */, 122 | 6E52293A29981CBD00836594 /* Utility */, 123 | 6EF2B6302962F90F0041242B /* Helper */, 124 | 6EF2B6282962E37B0041242B /* Models */, 125 | 6EF2B61D2962D7D00041242B /* Scene */, 126 | 6E05E3E3291C2AD800B08F4B /* LaunchScreen.storyboard */, 127 | 6E05E3E6291C2AD800B08F4B /* Info.plist */, 128 | ); 129 | path = SimpleVIPExample; 130 | sourceTree = ""; 131 | }; 132 | 6E05E3EC291C2E2100B08F4B /* AppDelegate */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 6E05E3D8291C2AD700B08F4B /* AppDelegate.swift */, 136 | 6E05E3DA291C2AD700B08F4B /* SceneDelegate.swift */, 137 | ); 138 | path = AppDelegate; 139 | sourceTree = ""; 140 | }; 141 | 6E52293A29981CBD00836594 /* Utility */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 6E52293C29981DE900836594 /* EndPoint.swift */, 145 | 6E52293E2998205C00836594 /* ProductEndPoint.swift */, 146 | ); 147 | path = Utility; 148 | sourceTree = ""; 149 | }; 150 | 6E5229432998255200836594 /* Constants */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 6EF2B6312962F9370041242B /* Constants.swift */, 154 | ); 155 | path = Constants; 156 | sourceTree = ""; 157 | }; 158 | 6E78EBF22964193B0046C6F8 /* ProductsScene */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 6EF2B6332962F9CC0041242B /* View */, 162 | 6E811D552999030F00214B50 /* ProductsViewController.swift */, 163 | 6E811D5B29990CB000214B50 /* ProductsInteractor.swift */, 164 | 6E811D5D2999105700214B50 /* ProductsPresenter.swift */, 165 | 6E811D5F299911F800214B50 /* ProductsDataSource.swift */, 166 | 6E811D632999137700214B50 /* ProductsRouter.swift */, 167 | 6E811D612999131B00214B50 /* ProductsWorker.swift */, 168 | 6E811D57299904D200214B50 /* ProductsInterfaces.swift */, 169 | 6E811D592999067B00214B50 /* ProductsConfigurator.swift */, 170 | ); 171 | path = ProductsScene; 172 | sourceTree = ""; 173 | }; 174 | 6E78EBF329641D310046C6F8 /* ProductDetails */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 6E78EBF42964203E0046C6F8 /* ProductDetailsViewController.swift */, 178 | 6E78EBFA296429910046C6F8 /* ProductDetailsInteractor.swift */, 179 | 6E78EBFC29642A140046C6F8 /* ProductDetailsPresenter.swift */, 180 | 6E78EBF6296425E00046C6F8 /* ProductDetailsConfigurator.swift */, 181 | 6E78EBF8296428710046C6F8 /* ProductDetailsInterfaces.swift */, 182 | ); 183 | path = ProductDetails; 184 | sourceTree = ""; 185 | }; 186 | 6E811D6A2999430A00214B50 /* Extension */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 6E811D6B2999432800214B50 /* UIImageView+Extension.swift */, 190 | ); 191 | path = Extension; 192 | sourceTree = ""; 193 | }; 194 | 6EF2B61A2962D7420041242B /* Resource */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | 6E05E3E1291C2AD800B08F4B /* Assets.xcassets */, 198 | ); 199 | path = Resource; 200 | sourceTree = ""; 201 | }; 202 | 6EF2B61D2962D7D00041242B /* Scene */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 6E78EBF22964193B0046C6F8 /* ProductsScene */, 206 | 6E78EBF329641D310046C6F8 /* ProductDetails */, 207 | ); 208 | path = Scene; 209 | sourceTree = ""; 210 | }; 211 | 6EF2B6282962E37B0041242B /* Models */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 6E5229462998323A00836594 /* Product.swift */, 215 | ); 216 | path = Models; 217 | sourceTree = ""; 218 | }; 219 | 6EF2B6302962F90F0041242B /* Helper */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 6E5229412998252900836594 /* APIManager.swift */, 223 | 6E5229442998284900836594 /* CustomErrors.swift */, 224 | ); 225 | path = Helper; 226 | sourceTree = ""; 227 | }; 228 | 6EF2B6332962F9CC0041242B /* View */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 6E811D6529992AB400214B50 /* ProductTableViewCell.swift */, 232 | ); 233 | path = View; 234 | sourceTree = ""; 235 | }; 236 | /* End PBXGroup section */ 237 | 238 | /* Begin PBXNativeTarget section */ 239 | 6E05E3D4291C2AD700B08F4B /* SimpleVIPExample */ = { 240 | isa = PBXNativeTarget; 241 | buildConfigurationList = 6E05E3E9291C2AD800B08F4B /* Build configuration list for PBXNativeTarget "SimpleVIPExample" */; 242 | buildPhases = ( 243 | 6E05E3D1291C2AD700B08F4B /* Sources */, 244 | 6E05E3D2291C2AD700B08F4B /* Frameworks */, 245 | 6E05E3D3291C2AD700B08F4B /* Resources */, 246 | ); 247 | buildRules = ( 248 | ); 249 | dependencies = ( 250 | ); 251 | name = SimpleVIPExample; 252 | packageProductDependencies = ( 253 | 6E811D68299940B700214B50 /* Kingfisher */, 254 | ); 255 | productName = SimpleVIPExample; 256 | productReference = 6E05E3D5291C2AD700B08F4B /* SimpleVIPExample.app */; 257 | productType = "com.apple.product-type.application"; 258 | }; 259 | 6EA5C4E82965CB53008D1927 /* SimpleVIPExampleTests */ = { 260 | isa = PBXNativeTarget; 261 | buildConfigurationList = 6EA5C4F12965CB53008D1927 /* Build configuration list for PBXNativeTarget "SimpleVIPExampleTests" */; 262 | buildPhases = ( 263 | 6EA5C4E52965CB53008D1927 /* Sources */, 264 | 6EA5C4E62965CB53008D1927 /* Frameworks */, 265 | 6EA5C4E72965CB53008D1927 /* Resources */, 266 | ); 267 | buildRules = ( 268 | ); 269 | dependencies = ( 270 | 6EA5C4EE2965CB53008D1927 /* PBXTargetDependency */, 271 | ); 272 | name = SimpleVIPExampleTests; 273 | productName = SimpleVIPExampleTests; 274 | productReference = 6EA5C4E92965CB53008D1927 /* SimpleVIPExampleTests.xctest */; 275 | productType = "com.apple.product-type.bundle.unit-test"; 276 | }; 277 | /* End PBXNativeTarget section */ 278 | 279 | /* Begin PBXProject section */ 280 | 6E05E3CD291C2AD700B08F4B /* Project object */ = { 281 | isa = PBXProject; 282 | attributes = { 283 | BuildIndependentTargetsInParallel = 1; 284 | LastSwiftUpdateCheck = 1400; 285 | LastUpgradeCheck = 1400; 286 | TargetAttributes = { 287 | 6E05E3D4291C2AD700B08F4B = { 288 | CreatedOnToolsVersion = 14.0; 289 | }; 290 | 6EA5C4E82965CB53008D1927 = { 291 | CreatedOnToolsVersion = 14.0; 292 | LastSwiftMigration = 1400; 293 | TestTargetID = 6E05E3D4291C2AD700B08F4B; 294 | }; 295 | }; 296 | }; 297 | buildConfigurationList = 6E05E3D0291C2AD700B08F4B /* Build configuration list for PBXProject "SimpleVIPExample" */; 298 | compatibilityVersion = "Xcode 14.0"; 299 | developmentRegion = en; 300 | hasScannedForEncodings = 0; 301 | knownRegions = ( 302 | en, 303 | Base, 304 | ); 305 | mainGroup = 6E05E3CC291C2AD700B08F4B; 306 | packageReferences = ( 307 | 6E811D67299940B700214B50 /* XCRemoteSwiftPackageReference "Kingfisher" */, 308 | ); 309 | productRefGroup = 6E05E3D6291C2AD700B08F4B /* Products */; 310 | projectDirPath = ""; 311 | projectRoot = ""; 312 | targets = ( 313 | 6E05E3D4291C2AD700B08F4B /* SimpleVIPExample */, 314 | 6EA5C4E82965CB53008D1927 /* SimpleVIPExampleTests */, 315 | ); 316 | }; 317 | /* End PBXProject section */ 318 | 319 | /* Begin PBXResourcesBuildPhase section */ 320 | 6E05E3D3291C2AD700B08F4B /* Resources */ = { 321 | isa = PBXResourcesBuildPhase; 322 | buildActionMask = 2147483647; 323 | files = ( 324 | 6E05E3E5291C2AD800B08F4B /* LaunchScreen.storyboard in Resources */, 325 | 6E05E3E2291C2AD800B08F4B /* Assets.xcassets in Resources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | 6EA5C4E72965CB53008D1927 /* Resources */ = { 330 | isa = PBXResourcesBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | }; 336 | /* End PBXResourcesBuildPhase section */ 337 | 338 | /* Begin PBXSourcesBuildPhase section */ 339 | 6E05E3D1291C2AD700B08F4B /* Sources */ = { 340 | isa = PBXSourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | 6E811D60299911F800214B50 /* ProductsDataSource.swift in Sources */, 344 | 6EF2B6322962F9370041242B /* Constants.swift in Sources */, 345 | 6E811D642999137700214B50 /* ProductsRouter.swift in Sources */, 346 | 6E5229422998252900836594 /* APIManager.swift in Sources */, 347 | 6E811D58299904D200214B50 /* ProductsInterfaces.swift in Sources */, 348 | 6E811D5C29990CB000214B50 /* ProductsInteractor.swift in Sources */, 349 | 6E52293D29981DE900836594 /* EndPoint.swift in Sources */, 350 | 6E78EBFD29642A140046C6F8 /* ProductDetailsPresenter.swift in Sources */, 351 | 6E78EBF52964203E0046C6F8 /* ProductDetailsViewController.swift in Sources */, 352 | 6E05E3D9291C2AD700B08F4B /* AppDelegate.swift in Sources */, 353 | 6E78EBFB296429910046C6F8 /* ProductDetailsInteractor.swift in Sources */, 354 | 6E811D5A2999067B00214B50 /* ProductsConfigurator.swift in Sources */, 355 | 6E811D562999030F00214B50 /* ProductsViewController.swift in Sources */, 356 | 6E05E3DB291C2AD700B08F4B /* SceneDelegate.swift in Sources */, 357 | 6E811D5E2999105700214B50 /* ProductsPresenter.swift in Sources */, 358 | 6E78EBF7296425E00046C6F8 /* ProductDetailsConfigurator.swift in Sources */, 359 | 6E811D622999131B00214B50 /* ProductsWorker.swift in Sources */, 360 | 6E5229472998323A00836594 /* Product.swift in Sources */, 361 | 6E52293F2998205C00836594 /* ProductEndPoint.swift in Sources */, 362 | 6E811D6629992AB400214B50 /* ProductTableViewCell.swift in Sources */, 363 | 6E78EBF9296428710046C6F8 /* ProductDetailsInterfaces.swift in Sources */, 364 | 6E811D6C2999432800214B50 /* UIImageView+Extension.swift in Sources */, 365 | 6E5229452998284900836594 /* CustomErrors.swift in Sources */, 366 | ); 367 | runOnlyForDeploymentPostprocessing = 0; 368 | }; 369 | 6EA5C4E52965CB53008D1927 /* Sources */ = { 370 | isa = PBXSourcesBuildPhase; 371 | buildActionMask = 2147483647; 372 | files = ( 373 | ); 374 | runOnlyForDeploymentPostprocessing = 0; 375 | }; 376 | /* End PBXSourcesBuildPhase section */ 377 | 378 | /* Begin PBXTargetDependency section */ 379 | 6EA5C4EE2965CB53008D1927 /* PBXTargetDependency */ = { 380 | isa = PBXTargetDependency; 381 | target = 6E05E3D4291C2AD700B08F4B /* SimpleVIPExample */; 382 | targetProxy = 6EA5C4ED2965CB53008D1927 /* PBXContainerItemProxy */; 383 | }; 384 | /* End PBXTargetDependency section */ 385 | 386 | /* Begin PBXVariantGroup section */ 387 | 6E05E3E3291C2AD800B08F4B /* LaunchScreen.storyboard */ = { 388 | isa = PBXVariantGroup; 389 | children = ( 390 | 6E05E3E4291C2AD800B08F4B /* Base */, 391 | ); 392 | name = LaunchScreen.storyboard; 393 | sourceTree = ""; 394 | }; 395 | /* End PBXVariantGroup section */ 396 | 397 | /* Begin XCBuildConfiguration section */ 398 | 6E05E3E7291C2AD800B08F4B /* Debug */ = { 399 | isa = XCBuildConfiguration; 400 | buildSettings = { 401 | ALWAYS_SEARCH_USER_PATHS = NO; 402 | CLANG_ANALYZER_NONNULL = YES; 403 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 405 | CLANG_ENABLE_MODULES = YES; 406 | CLANG_ENABLE_OBJC_ARC = YES; 407 | CLANG_ENABLE_OBJC_WEAK = YES; 408 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 409 | CLANG_WARN_BOOL_CONVERSION = YES; 410 | CLANG_WARN_COMMA = YES; 411 | CLANG_WARN_CONSTANT_CONVERSION = YES; 412 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 415 | CLANG_WARN_EMPTY_BODY = YES; 416 | CLANG_WARN_ENUM_CONVERSION = YES; 417 | CLANG_WARN_INFINITE_RECURSION = YES; 418 | CLANG_WARN_INT_CONVERSION = YES; 419 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 420 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 421 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 422 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 423 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 424 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 425 | CLANG_WARN_STRICT_PROTOTYPES = YES; 426 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 427 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 428 | CLANG_WARN_UNREACHABLE_CODE = YES; 429 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 430 | COPY_PHASE_STRIP = NO; 431 | DEBUG_INFORMATION_FORMAT = dwarf; 432 | ENABLE_STRICT_OBJC_MSGSEND = YES; 433 | ENABLE_TESTABILITY = YES; 434 | GCC_C_LANGUAGE_STANDARD = gnu11; 435 | GCC_DYNAMIC_NO_PIC = NO; 436 | GCC_NO_COMMON_BLOCKS = YES; 437 | GCC_OPTIMIZATION_LEVEL = 0; 438 | GCC_PREPROCESSOR_DEFINITIONS = ( 439 | "DEBUG=1", 440 | "$(inherited)", 441 | ); 442 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 443 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 444 | GCC_WARN_UNDECLARED_SELECTOR = YES; 445 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 446 | GCC_WARN_UNUSED_FUNCTION = YES; 447 | GCC_WARN_UNUSED_VARIABLE = YES; 448 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 449 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 450 | MTL_FAST_MATH = YES; 451 | ONLY_ACTIVE_ARCH = YES; 452 | SDKROOT = iphoneos; 453 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 454 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 455 | }; 456 | name = Debug; 457 | }; 458 | 6E05E3E8291C2AD800B08F4B /* Release */ = { 459 | isa = XCBuildConfiguration; 460 | buildSettings = { 461 | ALWAYS_SEARCH_USER_PATHS = NO; 462 | CLANG_ANALYZER_NONNULL = YES; 463 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 464 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 465 | CLANG_ENABLE_MODULES = YES; 466 | CLANG_ENABLE_OBJC_ARC = YES; 467 | CLANG_ENABLE_OBJC_WEAK = YES; 468 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 469 | CLANG_WARN_BOOL_CONVERSION = YES; 470 | CLANG_WARN_COMMA = YES; 471 | CLANG_WARN_CONSTANT_CONVERSION = YES; 472 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 473 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 474 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 475 | CLANG_WARN_EMPTY_BODY = YES; 476 | CLANG_WARN_ENUM_CONVERSION = YES; 477 | CLANG_WARN_INFINITE_RECURSION = YES; 478 | CLANG_WARN_INT_CONVERSION = YES; 479 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 480 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 481 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 482 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 483 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 484 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 485 | CLANG_WARN_STRICT_PROTOTYPES = YES; 486 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 487 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 488 | CLANG_WARN_UNREACHABLE_CODE = YES; 489 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 490 | COPY_PHASE_STRIP = NO; 491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 492 | ENABLE_NS_ASSERTIONS = NO; 493 | ENABLE_STRICT_OBJC_MSGSEND = YES; 494 | GCC_C_LANGUAGE_STANDARD = gnu11; 495 | GCC_NO_COMMON_BLOCKS = YES; 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 503 | MTL_ENABLE_DEBUG_INFO = NO; 504 | MTL_FAST_MATH = YES; 505 | SDKROOT = iphoneos; 506 | SWIFT_COMPILATION_MODE = wholemodule; 507 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 508 | VALIDATE_PRODUCT = YES; 509 | }; 510 | name = Release; 511 | }; 512 | 6E05E3EA291C2AD800B08F4B /* Debug */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 516 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 517 | CODE_SIGN_STYLE = Automatic; 518 | CURRENT_PROJECT_VERSION = 1; 519 | GENERATE_INFOPLIST_FILE = YES; 520 | INFOPLIST_FILE = SimpleVIPExample/Info.plist; 521 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 522 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 523 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 524 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 525 | LD_RUNPATH_SEARCH_PATHS = ( 526 | "$(inherited)", 527 | "@executable_path/Frameworks", 528 | ); 529 | MARKETING_VERSION = 1.0; 530 | PRODUCT_BUNDLE_IDENTIFIER = com.vishal.SimpleVIPExample; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_EMIT_LOC_STRINGS = YES; 533 | SWIFT_VERSION = 5.0; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | }; 536 | name = Debug; 537 | }; 538 | 6E05E3EB291C2AD800B08F4B /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 543 | CODE_SIGN_STYLE = Automatic; 544 | CURRENT_PROJECT_VERSION = 1; 545 | GENERATE_INFOPLIST_FILE = YES; 546 | INFOPLIST_FILE = SimpleVIPExample/Info.plist; 547 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 548 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 549 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 550 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 551 | LD_RUNPATH_SEARCH_PATHS = ( 552 | "$(inherited)", 553 | "@executable_path/Frameworks", 554 | ); 555 | MARKETING_VERSION = 1.0; 556 | PRODUCT_BUNDLE_IDENTIFIER = com.vishal.SimpleVIPExample; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | SWIFT_EMIT_LOC_STRINGS = YES; 559 | SWIFT_VERSION = 5.0; 560 | TARGETED_DEVICE_FAMILY = "1,2"; 561 | }; 562 | name = Release; 563 | }; 564 | 6EA5C4EF2965CB53008D1927 /* Debug */ = { 565 | isa = XCBuildConfiguration; 566 | buildSettings = { 567 | BUNDLE_LOADER = "$(TEST_HOST)"; 568 | CLANG_ENABLE_MODULES = YES; 569 | CODE_SIGN_STYLE = Automatic; 570 | CURRENT_PROJECT_VERSION = 1; 571 | GENERATE_INFOPLIST_FILE = YES; 572 | LD_RUNPATH_SEARCH_PATHS = ( 573 | "$(inherited)", 574 | "@executable_path/Frameworks", 575 | "@loader_path/Frameworks", 576 | ); 577 | MARKETING_VERSION = 1.0; 578 | PRODUCT_BUNDLE_IDENTIFIER = com.vishal.SimpleVIPExampleTests; 579 | PRODUCT_NAME = "$(TARGET_NAME)"; 580 | SWIFT_EMIT_LOC_STRINGS = NO; 581 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 582 | SWIFT_VERSION = 5.0; 583 | TARGETED_DEVICE_FAMILY = "1,2"; 584 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SimpleVIPExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SimpleVIPExample"; 585 | }; 586 | name = Debug; 587 | }; 588 | 6EA5C4F02965CB53008D1927 /* Release */ = { 589 | isa = XCBuildConfiguration; 590 | buildSettings = { 591 | BUNDLE_LOADER = "$(TEST_HOST)"; 592 | CLANG_ENABLE_MODULES = YES; 593 | CODE_SIGN_STYLE = Automatic; 594 | CURRENT_PROJECT_VERSION = 1; 595 | GENERATE_INFOPLIST_FILE = YES; 596 | LD_RUNPATH_SEARCH_PATHS = ( 597 | "$(inherited)", 598 | "@executable_path/Frameworks", 599 | "@loader_path/Frameworks", 600 | ); 601 | MARKETING_VERSION = 1.0; 602 | PRODUCT_BUNDLE_IDENTIFIER = com.vishal.SimpleVIPExampleTests; 603 | PRODUCT_NAME = "$(TARGET_NAME)"; 604 | SWIFT_EMIT_LOC_STRINGS = NO; 605 | SWIFT_VERSION = 5.0; 606 | TARGETED_DEVICE_FAMILY = "1,2"; 607 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SimpleVIPExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SimpleVIPExample"; 608 | }; 609 | name = Release; 610 | }; 611 | /* End XCBuildConfiguration section */ 612 | 613 | /* Begin XCConfigurationList section */ 614 | 6E05E3D0291C2AD700B08F4B /* Build configuration list for PBXProject "SimpleVIPExample" */ = { 615 | isa = XCConfigurationList; 616 | buildConfigurations = ( 617 | 6E05E3E7291C2AD800B08F4B /* Debug */, 618 | 6E05E3E8291C2AD800B08F4B /* Release */, 619 | ); 620 | defaultConfigurationIsVisible = 0; 621 | defaultConfigurationName = Release; 622 | }; 623 | 6E05E3E9291C2AD800B08F4B /* Build configuration list for PBXNativeTarget "SimpleVIPExample" */ = { 624 | isa = XCConfigurationList; 625 | buildConfigurations = ( 626 | 6E05E3EA291C2AD800B08F4B /* Debug */, 627 | 6E05E3EB291C2AD800B08F4B /* Release */, 628 | ); 629 | defaultConfigurationIsVisible = 0; 630 | defaultConfigurationName = Release; 631 | }; 632 | 6EA5C4F12965CB53008D1927 /* Build configuration list for PBXNativeTarget "SimpleVIPExampleTests" */ = { 633 | isa = XCConfigurationList; 634 | buildConfigurations = ( 635 | 6EA5C4EF2965CB53008D1927 /* Debug */, 636 | 6EA5C4F02965CB53008D1927 /* Release */, 637 | ); 638 | defaultConfigurationIsVisible = 0; 639 | defaultConfigurationName = Release; 640 | }; 641 | /* End XCConfigurationList section */ 642 | 643 | /* Begin XCRemoteSwiftPackageReference section */ 644 | 6E811D67299940B700214B50 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 645 | isa = XCRemoteSwiftPackageReference; 646 | repositoryURL = "https://github.com/onevcat/Kingfisher.git"; 647 | requirement = { 648 | kind = upToNextMajorVersion; 649 | minimumVersion = 7.0.0; 650 | }; 651 | }; 652 | /* End XCRemoteSwiftPackageReference section */ 653 | 654 | /* Begin XCSwiftPackageProductDependency section */ 655 | 6E811D68299940B700214B50 /* Kingfisher */ = { 656 | isa = XCSwiftPackageProductDependency; 657 | package = 6E811D67299940B700214B50 /* XCRemoteSwiftPackageReference "Kingfisher" */; 658 | productName = Kingfisher; 659 | }; 660 | /* End XCSwiftPackageProductDependency section */ 661 | }; 662 | rootObject = 6E05E3CD291C2AD700B08F4B /* Project object */; 663 | } 664 | -------------------------------------------------------------------------------- /SimpleVIPExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimpleVIPExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimpleVIPExample.xcodeproj/xcshareddata/xcschemes/SimpleVIPExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 57 | 63 | 64 | 65 | 66 | 72 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /SimpleVIPExample/AppDelegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 10/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /SimpleVIPExample/AppDelegate/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 10/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let windowScene = (scene as? UIWindowScene) else { return } 16 | let window = UIWindow(windowScene: windowScene) 17 | let usersDataViewController = ProductsConfigurator.createScene() 18 | let navigationController = UINavigationController(rootViewController: usersDataViewController) 19 | window.rootViewController = navigationController 20 | window.makeKeyAndVisible() 21 | self.window = window 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /SimpleVIPExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SimpleVIPExample/Constants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 02/01/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ViewDimensionConstants: CGFloat { 11 | case zero = 0 12 | case eight = 8.0 13 | case ten = 10.0 14 | case fifteen = 15.0 15 | case sixteen = 16.0 16 | case hundred = 100.0 17 | case oneHundredThirtyTwo = 132.0 18 | case twoHundred = 200.0 19 | } 20 | 21 | enum IntegerConstants: Int { 22 | case zero = 0 23 | case one = 1 24 | case two = 2 25 | case three = 3 26 | } 27 | 28 | enum StringConstant: String { 29 | case products = "Products" 30 | case productsDetails = "Products Details" 31 | } 32 | 33 | enum API { 34 | static let baseURl = "https://fakestoreapi.com/" 35 | } 36 | -------------------------------------------------------------------------------- /SimpleVIPExample/Extension/UIImageView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extension.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import UIKit 9 | import Kingfisher 10 | 11 | extension UIImageView { 12 | func setImage(with urlString: String) { 13 | guard let url = URL.init(string: urlString) else { 14 | return 15 | } 16 | let resource = ImageResource(downloadURL: url, cacheKey: urlString) 17 | kf.indicatorType = .activity 18 | kf.setImage(with: resource) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleVIPExample/Helper/APIManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIManager.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias RequestResult = (Result) -> Void 11 | 12 | final class APIManager { 13 | 14 | static let shared = APIManager() 15 | 16 | private init() {} 17 | 18 | static var sharedHeaders : HTTPHeaders { 19 | ["Content-Type": "application/json"] 20 | } 21 | 22 | func request(modelType: T.Type, type: EndPoint, completion: @escaping RequestResult) { 23 | 24 | guard let url = type.url else { 25 | completion(.failure(.invalidURL)) 26 | return 27 | } 28 | 29 | var request = URLRequest(url: url) 30 | 31 | request.httpMethod = type.method.rawValue 32 | 33 | request.allHTTPHeaderFields = type.headers 34 | 35 | URLSession.shared.dataTask(with: request) { data, response, error in 36 | guard let data, error == nil else { 37 | completion(.failure(.invalidData)) 38 | return 39 | } 40 | 41 | guard let response = response as? HTTPURLResponse, 42 | 200 ... 299 ~= response.statusCode else { 43 | completion(.failure(.invalidResponse)) 44 | return 45 | } 46 | do { 47 | let model = try JSONDecoder().decode(modelType, from: data) 48 | completion(.success(model)) 49 | }catch { 50 | completion(.failure(.network(error))) 51 | } 52 | 53 | }.resume() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SimpleVIPExample/Helper/CustomErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomErrors.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CustomErrors: Error { 11 | case invalidResponse 12 | case invalidURL 13 | case invalidData 14 | case network(Error?) 15 | } 16 | 17 | // Uncomment it to pass LocalizedError 18 | 19 | /* 20 | extension CustomErrors: LocalizedError { 21 | 22 | public var errorDescription: String? { 23 | return nil 24 | } 25 | } 26 | */ 27 | -------------------------------------------------------------------------------- /SimpleVIPExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SimpleVIPExample/Models/Product.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Product.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Product: Codable { 11 | let id: Int 12 | let title: String 13 | let price: Double 14 | let description: String 15 | let category: String 16 | let image: String 17 | let rating: Rate 18 | } 19 | 20 | struct Rate: Codable { 21 | let rate: Double 22 | let count: Int 23 | } 24 | -------------------------------------------------------------------------------- /SimpleVIPExample/Resource/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 | -------------------------------------------------------------------------------- /SimpleVIPExample/Resource/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 | -------------------------------------------------------------------------------- /SimpleVIPExample/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductDetails/ProductDetailsConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailsConfigurator.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 03/01/23. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | 12 | struct ProductDetailsConfigurator { 13 | 14 | static func createScene(product: Product) -> ProductDetailsViewController { 15 | let viewController = ProductDetailsViewController(product: product) 16 | let presenter = ProductDetailsPresenter() 17 | let interactor = ProductDetailsInteractor() 18 | viewController.interactor = interactor 19 | interactor.presenter = presenter 20 | presenter.viewController = viewController 21 | return viewController 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductDetails/ProductDetailsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailsInteractor.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 03/01/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProductDetailsInteractor: ProductDetailsBusinessLogic { 11 | // MARK: - Properties 12 | var presenter: ProductDetailsPresentationLogic? 13 | 14 | func setupProductDetails(_ product: Product) { 15 | presenter?.presentProductDetailsData(product) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductDetails/ProductDetailsInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailsInterfaces.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 03/01/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductDetailsBusinessLogic { 11 | func setupProductDetails(_ product: Product) 12 | } 13 | 14 | protocol ProductDetailsPresentationLogic { 15 | func presentProductDetailsData(_ product: Product) 16 | } 17 | 18 | protocol ProductDetailsDisplayLogic: AnyObject { 19 | func displayProductDetails(_ product: Product) 20 | } 21 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductDetails/ProductDetailsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersDetailsPresenter.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 03/01/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProductDetailsPresenter: ProductDetailsPresentationLogic { 11 | // MARK: - Properties 12 | weak var viewController: ProductDetailsDisplayLogic? 13 | 14 | func presentProductDetailsData(_ product: Product) { 15 | viewController?.displayProductDetails(product) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductDetails/ProductDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailsViewController.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 03/01/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProductDetailsViewController: UIViewController { 11 | 12 | // MARK: - Component 13 | private lazy var containerVStack: UIStackView = { 14 | let stackView = UIStackView() 15 | stackView.translatesAutoresizingMaskIntoConstraints = false 16 | stackView.axis = .vertical 17 | stackView.alignment = .center 18 | stackView.spacing = ViewDimensionConstants.sixteen.rawValue 19 | return stackView 20 | }() 21 | 22 | private lazy var productImageView: UIImageView = { 23 | let imageView = UIImageView() 24 | imageView.translatesAutoresizingMaskIntoConstraints = false 25 | imageView.backgroundColor = .lightGray 26 | imageView.layer.cornerRadius = ViewDimensionConstants.ten.rawValue 27 | imageView.layer.masksToBounds = true 28 | return imageView 29 | }() 30 | 31 | private lazy var titleLabel: UILabel = { 32 | let label = UILabel() 33 | label.translatesAutoresizingMaskIntoConstraints = false 34 | label.font = .boldSystemFont(ofSize: 25) 35 | label.textAlignment = .center 36 | label.textColor = .label 37 | label.numberOfLines = IntegerConstants.zero.rawValue 38 | return label 39 | }() 40 | 41 | private lazy var descLabel: UILabel = { 42 | let label = UILabel() 43 | label.translatesAutoresizingMaskIntoConstraints = false 44 | label.font = .systemFont(ofSize: 20) 45 | label.textAlignment = .center 46 | label.textColor = .secondaryLabel 47 | label.numberOfLines = IntegerConstants.zero.rawValue 48 | return label 49 | }() 50 | 51 | // MARK: - Properties 52 | var interactor: ProductDetailsBusinessLogic? 53 | var product: Product 54 | 55 | init(product: Product) { 56 | self.product = product 57 | super.init(nibName: nil, bundle: nil) 58 | } 59 | 60 | required init?(coder: NSCoder) { 61 | fatalError("init(coder:) has not been implemented") 62 | } 63 | 64 | // MARK: - Life Cycle 65 | override func viewDidLoad() { 66 | super.viewDidLoad() 67 | view.backgroundColor = .systemBackground 68 | title = StringConstant.productsDetails.rawValue 69 | setupView() 70 | interactor?.setupProductDetails(product) 71 | } 72 | 73 | } 74 | 75 | // MARK: - UserDetailsDisplayLogic 76 | extension ProductDetailsViewController: ProductDetailsDisplayLogic { 77 | 78 | func displayProductDetails(_ product: Product) { 79 | titleLabel.text = product.title 80 | descLabel.text = product.description 81 | productImageView.setImage(with: product.image) 82 | } 83 | } 84 | 85 | // MARK: - Helping Methods 86 | extension ProductDetailsViewController { 87 | 88 | private func setupView() { 89 | view.addSubview(containerVStack) 90 | NSLayoutConstraint.activate([ 91 | containerVStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, 92 | constant: ViewDimensionConstants.sixteen.rawValue), 93 | containerVStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, 94 | constant: -ViewDimensionConstants.sixteen.rawValue), 95 | containerVStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, 96 | constant: ViewDimensionConstants.sixteen.rawValue), 97 | productImageView.widthAnchor.constraint(equalToConstant: ViewDimensionConstants.twoHundred.rawValue), 98 | productImageView.heightAnchor.constraint(equalToConstant: ViewDimensionConstants.twoHundred.rawValue) 99 | ]) 100 | containerVStack.addArrangedSubview(productImageView) 101 | containerVStack.addArrangedSubview(titleLabel) 102 | containerVStack.addArrangedSubview(descLabel) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsConfigurator.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ProductsConfigurator { 11 | 12 | static func createScene() -> ProductsViewController { 13 | let viewController = ProductsViewController() 14 | let presenter = ProductsPresenter() 15 | let interactor = ProductsInteractor() 16 | let router = ProductsRouter() 17 | let worker = ProductsWorker() 18 | viewController.interactor = interactor 19 | interactor.presenter = presenter 20 | interactor.worker = worker 21 | presenter.viewController = viewController 22 | viewController.router = router 23 | router.viewController = viewController 24 | return viewController 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsDataSource.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import UIKit 9 | 10 | enum ProductsCells: String { 11 | case productTableViewCell 12 | } 13 | 14 | class ProductsDataSource: NSObject { 15 | 16 | // MARK: - Properties 17 | var products: [Product] 18 | weak var delegate: ProductsDataSourceDelegate? 19 | 20 | // MARK: - init 21 | init(_ products: [Product]) { 22 | self.products = products 23 | } 24 | 25 | } 26 | 27 | // MARK: - UITableViewDataSource 28 | extension ProductsDataSource: UITableViewDataSource { 29 | 30 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 31 | products.count 32 | } 33 | 34 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 35 | guard let cell = tableView.dequeueReusableCell(withIdentifier: ProductsCells.productTableViewCell.rawValue) as? ProductTableViewCell else { return UITableViewCell() } 36 | let product = products[indexPath.row] 37 | let model = ProductTableViewCell.ViewModel(title: product.title, description: product.description, image: product.image) 38 | cell.updateData(model) 39 | return cell 40 | } 41 | 42 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 43 | return ViewDimensionConstants.oneHundredThirtyTwo.rawValue 44 | } 45 | } 46 | 47 | // MARK: - UITableViewDelegate 48 | extension ProductsDataSource: UITableViewDelegate { 49 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 50 | let product = products[indexPath.row] 51 | delegate?.selectedProduct(product) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsInteractor.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class ProductsInteractor: ProductsBusinessLogic { 11 | // MARK: - Properties 12 | var presenter: ProductsPresentationLogic? 13 | var worker: ProductsWorker? 14 | 15 | func requestProducts() { 16 | presenter?.updateActivityIndicator(status: .start) 17 | worker?.getProducts(completion: { [weak self] result in 18 | self?.presenter?.updateActivityIndicator(status: .stop) 19 | switch result { 20 | case .success(let products): 21 | print(products) 22 | self?.presenter?.presentProductsData(products) 23 | case .failure(let error): 24 | print(error) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsInterfaces.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProductsBusinessLogic { 11 | func requestProducts() 12 | } 13 | 14 | protocol ProductsPresentationLogic { 15 | func presentProductsData(_ products: [Product]) 16 | func updateActivityIndicator(status: IndicatorStatus) 17 | } 18 | 19 | protocol ProductsDisplayLogic: AnyObject { 20 | func displayProducts(productsDataSource: ProductsDataSource) 21 | func updateActivityIndicatorInProductsPage(status: IndicatorStatus) 22 | } 23 | 24 | protocol ProductsRoutingLogic { 25 | func navigateToProductDetailsController(_ product: Product) 26 | } 27 | 28 | protocol ProductsDataSourceDelegate: AnyObject { 29 | func selectedProduct(_ product: Product) 30 | } 31 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsPresenter.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class ProductsPresenter: ProductsPresentationLogic { 11 | 12 | // MARK: - Properties 13 | weak var viewController: ProductsDisplayLogic? 14 | 15 | func presentProductsData(_ products: [Product]) { 16 | let dataSource = ProductsDataSource(products) 17 | viewController?.displayProducts(productsDataSource: dataSource) 18 | } 19 | 20 | func updateActivityIndicator(status: IndicatorStatus) { 21 | viewController?.updateActivityIndicatorInProductsPage(status: status) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsRouter.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProductsRouter: ProductsRoutingLogic { 11 | // MARK: - Properties 12 | weak var viewController: ProductsViewController? 13 | 14 | func navigateToProductDetailsController(_ product: Product) { 15 | let vc = ProductDetailsConfigurator.createScene(product: product) 16 | viewController?.navigationController?.pushViewController(vc, animated: true) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsViewController.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import UIKit 9 | 10 | enum IndicatorStatus { 11 | case start 12 | case stop 13 | } 14 | 15 | 16 | class ProductsViewController: UIViewController { 17 | 18 | // MARK: - Component 19 | lazy var tableView: UITableView = { 20 | let tableView = UITableView() 21 | tableView.translatesAutoresizingMaskIntoConstraints = false 22 | tableView.showsVerticalScrollIndicator = false 23 | tableView.separatorStyle = .none 24 | tableView.alwaysBounceVertical = false 25 | tableView.register(ProductTableViewCell.self, forCellReuseIdentifier: ProductsCells.productTableViewCell.rawValue) 26 | return tableView 27 | }() 28 | 29 | lazy var indicatorView: UIActivityIndicatorView = { 30 | let view = UIActivityIndicatorView(style: .large) 31 | view.color = .orange 32 | view.translatesAutoresizingMaskIntoConstraints = false 33 | return view 34 | }() 35 | 36 | // MARK: - Properties 37 | var interactor: ProductsBusinessLogic? 38 | var dataSource: ProductsDataSource? 39 | var router: ProductsRoutingLogic? 40 | 41 | // MARK: - Life Cycle 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | view.backgroundColor = .systemBackground 45 | navigationController?.navigationBar.prefersLargeTitles = true 46 | navigationItem.largeTitleDisplayMode = .always 47 | title = StringConstant.products.rawValue 48 | setupView() 49 | interactor?.requestProducts() 50 | } 51 | } 52 | 53 | // MARK: - ProductsDisplayLogic 54 | extension ProductsViewController: ProductsDisplayLogic { 55 | 56 | func displayProducts(productsDataSource: ProductsDataSource) { 57 | DispatchQueue.main.async { 58 | self.dataSource = productsDataSource 59 | self.tableView.dataSource = self.dataSource 60 | self.tableView.delegate = self.dataSource 61 | self.dataSource?.delegate = self 62 | self.tableView.reloadData() 63 | } 64 | } 65 | 66 | func updateActivityIndicatorInProductsPage(status: IndicatorStatus) { 67 | DispatchQueue.main.async { 68 | status == .start ? self.indicatorView.startAnimating() : self.indicatorView.stopAnimating() 69 | } 70 | } 71 | } 72 | 73 | // MARK: - ProductsDataSourceDelegate 74 | extension ProductsViewController: ProductsDataSourceDelegate { 75 | func selectedProduct(_ product: Product) { 76 | router?.navigateToProductDetailsController(product) 77 | } 78 | } 79 | 80 | // MARK: - Helping Methods 81 | extension ProductsViewController { 82 | 83 | private func setupView() { 84 | view.addSubview(tableView) 85 | view.addSubview(indicatorView) 86 | NSLayoutConstraint.activate([ 87 | tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, 88 | constant: ViewDimensionConstants.zero.rawValue), 89 | tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, 90 | constant: -ViewDimensionConstants.zero.rawValue), 91 | tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, 92 | constant: ViewDimensionConstants.zero.rawValue), 93 | tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, 94 | constant: -ViewDimensionConstants.zero.rawValue), 95 | indicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 96 | indicatorView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 97 | ]) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/ProductsWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsWorker.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias ProductResult = Result<[Product], CustomErrors> 11 | 12 | class ProductsWorker { 13 | 14 | func getProducts(completion: @escaping (ProductResult) -> Void) { 15 | APIManager.shared.request ( 16 | modelType: [Product].self, 17 | type: ProductEndPoint.getProducts) { result in 18 | switch result { 19 | case .success(let products): 20 | completion(.success(products)) 21 | case .failure(let error): 22 | completion(.failure(error)) 23 | } 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /SimpleVIPExample/Scene/ProductsScene/View/ProductTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductTableViewCell.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProductTableViewCell: UITableViewCell { 11 | // MARK: - Layout 12 | struct Layout { 13 | var topPadding: CGFloat = ViewDimensionConstants.eight.rawValue 14 | var bottomPadding: CGFloat = ViewDimensionConstants.eight.rawValue 15 | var leftPadding: CGFloat = ViewDimensionConstants.eight.rawValue 16 | var rightPadding: CGFloat = ViewDimensionConstants.eight.rawValue 17 | 18 | init(topPadding: CGFloat = ViewDimensionConstants.eight.rawValue, 19 | bottomPadding: CGFloat = -ViewDimensionConstants.eight.rawValue, 20 | leftPadding: CGFloat = ViewDimensionConstants.eight.rawValue, 21 | rightPadding: CGFloat = -ViewDimensionConstants.eight.rawValue) { 22 | self.topPadding = topPadding 23 | self.bottomPadding = bottomPadding 24 | self.leftPadding = leftPadding 25 | self.rightPadding = rightPadding 26 | } 27 | } 28 | // MARK: - ViewModel 29 | struct ViewModel { 30 | var title: String? 31 | var description: String? 32 | var image: String? 33 | } 34 | 35 | // MARK: - Component 36 | private lazy var containerView: UIView = { 37 | let view = UIView() 38 | view.translatesAutoresizingMaskIntoConstraints = false 39 | view.backgroundColor = .systemGray6 40 | view.layer.cornerRadius = ViewDimensionConstants.fifteen.rawValue 41 | view.layer.masksToBounds = true 42 | return view 43 | }() 44 | 45 | private lazy var containerHStack: UIStackView = { 46 | let stackView = UIStackView() 47 | stackView.translatesAutoresizingMaskIntoConstraints = false 48 | stackView.axis = .horizontal 49 | stackView.spacing = ViewDimensionConstants.eight.rawValue 50 | return stackView 51 | }() 52 | 53 | private lazy var productImageView: UIImageView = { 54 | let imageView = UIImageView() 55 | imageView.translatesAutoresizingMaskIntoConstraints = false 56 | imageView.backgroundColor = .lightGray 57 | imageView.layer.cornerRadius = ViewDimensionConstants.ten.rawValue 58 | imageView.layer.masksToBounds = true 59 | return imageView 60 | }() 61 | 62 | private lazy var containerVStack: UIStackView = { 63 | let stackView = UIStackView() 64 | stackView.translatesAutoresizingMaskIntoConstraints = false 65 | stackView.axis = .vertical 66 | stackView.distribution = .fill 67 | stackView.spacing = ViewDimensionConstants.eight.rawValue 68 | return stackView 69 | }() 70 | 71 | private lazy var titleLabel: UILabel = { 72 | let label = UILabel() 73 | label.translatesAutoresizingMaskIntoConstraints = false 74 | label.font = .boldSystemFont(ofSize: 16) 75 | label.textAlignment = .left 76 | label.textColor = .label 77 | label.numberOfLines = IntegerConstants.two.rawValue 78 | return label 79 | }() 80 | 81 | private lazy var descLabel: UILabel = { 82 | let label = UILabel() 83 | label.translatesAutoresizingMaskIntoConstraints = false 84 | label.font = .systemFont(ofSize: 12) 85 | label.textAlignment = .left 86 | label.textColor = .secondaryLabel 87 | label.numberOfLines = IntegerConstants.three.rawValue 88 | return label 89 | }() 90 | 91 | // MARK: - Properties 92 | private var topConstraint: NSLayoutConstraint? 93 | private var bottomConstraint: NSLayoutConstraint? 94 | private var leftConstraint: NSLayoutConstraint? 95 | private var rightConstraint: NSLayoutConstraint? 96 | private var layout = Layout() 97 | 98 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 99 | super.init(style: style, reuseIdentifier: reuseIdentifier) 100 | self.selectionStyle = .none 101 | self.separatorInset = .zero 102 | setupUI() 103 | } 104 | 105 | required init?(coder: NSCoder) { 106 | fatalError("init(coder:) has not been implemented") 107 | } 108 | } 109 | 110 | // MARK: - View Setup 111 | extension ProductTableViewCell { 112 | 113 | private func setupUI() { 114 | contentView.addSubview(containerView) 115 | leftConstraint = containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: layout.leftPadding) 116 | leftConstraint?.isActive = true 117 | 118 | rightConstraint = containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: layout.rightPadding) 119 | rightConstraint?.isActive = true 120 | 121 | topConstraint = containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: layout.topPadding) 122 | topConstraint?.isActive = true 123 | 124 | bottomConstraint = containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: layout.bottomPadding) 125 | bottomConstraint?.isActive = true 126 | 127 | containerView.addSubview(containerHStack) 128 | 129 | NSLayoutConstraint.activate([ 130 | containerHStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: ViewDimensionConstants.eight.rawValue), 131 | containerHStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -ViewDimensionConstants.eight.rawValue), 132 | containerHStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: ViewDimensionConstants.eight.rawValue), 133 | containerHStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -ViewDimensionConstants.eight.rawValue), 134 | productImageView.widthAnchor.constraint(equalToConstant: ViewDimensionConstants.hundred.rawValue) 135 | ]) 136 | 137 | containerHStack.addArrangedSubview(productImageView) 138 | containerHStack.addArrangedSubview(containerVStack) 139 | containerVStack.addArrangedSubview(titleLabel) 140 | containerVStack.addArrangedSubview(descLabel) 141 | } 142 | 143 | func updateLayout( 144 | topPadding: CGFloat = ViewDimensionConstants.eight.rawValue, 145 | bottomPadding: CGFloat = -ViewDimensionConstants.eight.rawValue, 146 | leftPadding: CGFloat = ViewDimensionConstants.eight.rawValue, 147 | rightPadding: CGFloat = -ViewDimensionConstants.eight.rawValue) { 148 | layout = Layout(topPadding: topPadding, bottomPadding: bottomPadding, leftPadding: leftPadding, rightPadding: rightPadding) 149 | topConstraint?.constant = layout.topPadding 150 | bottomConstraint?.constant = layout.bottomPadding 151 | leftConstraint?.constant = layout.leftPadding 152 | rightConstraint?.constant = layout.rightPadding 153 | } 154 | } 155 | 156 | // MARK: - Helping Methods for setup data 157 | extension ProductTableViewCell { 158 | 159 | func updateData( _ model: ViewModel) { 160 | titleLabel.text = model.title 161 | descLabel.text = model.description 162 | guard let image = model.image else { return } 163 | productImageView.setImage(with: image) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /SimpleVIPExample/Utility/EndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EndPoint.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias HTTPHeaders = [String: String] 11 | 12 | enum HTTPMethods: String { 13 | case get = "GET" 14 | case post = "POST" 15 | } 16 | 17 | protocol EndPoint { 18 | var path: String { get } 19 | var baseURL: String { get } 20 | var url: URL? { get } 21 | var method: HTTPMethods { get } 22 | var headers: HTTPHeaders? { get } 23 | } 24 | -------------------------------------------------------------------------------- /SimpleVIPExample/Utility/ProductEndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductEndPoint.swift 3 | // SimpleVIPExample 4 | // 5 | // Created by Vishal_Malvi on 12/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ProductEndPoint { 11 | case getProducts // Right now, I am implementing get functionality only if want we can add more cases 12 | } 13 | 14 | extension ProductEndPoint: EndPoint { 15 | 16 | var path: String { 17 | StringConstant.products.rawValue 18 | } 19 | 20 | var baseURL: String { 21 | API.baseURl 22 | } 23 | 24 | var url: URL? { 25 | URL(string: "\(baseURL)\(path)") 26 | } 27 | 28 | var method: HTTPMethods { 29 | .get 30 | } 31 | 32 | var headers: HTTPHeaders? { 33 | APIManager.sharedHeaders 34 | } 35 | } 36 | --------------------------------------------------------------------------------