├── .gitignore ├── LICENSE ├── ModuleArchitectureDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ModuleArchitectureDemo ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Helpers │ ├── Bundle.swift │ ├── URLExtensions.swift │ ├── VCExtensions.swift │ └── WeakContainer.swift ├── Info.plist ├── Modules │ ├── Login │ │ ├── LoginInteractor.swift │ │ ├── LoginModule.swift │ │ ├── LoginPresenter.swift │ │ ├── LoginViewController.swift │ │ └── LoginWireframe.swift │ ├── NonConformingModule.swift │ └── Payments │ │ ├── PaymentsInteractor.swift │ │ ├── PaymentsModule.swift │ │ ├── PaymentsPresenter.swift │ │ ├── PaymentsViewController.swift │ │ └── PaymentsWireframe.swift ├── NetworkService.swift ├── Routing │ ├── ApplicationRouter.swift │ ├── ApplicationServices.swift │ ├── Module.swift │ └── WireframeType.swift ├── Storyboards │ ├── LoginStoryboard.storyboard │ └── PaymentsStoryboard.storyboard └── ViewController.swift ├── ModuleArchitectureDemoTests ├── APIClientArchitectureDemoTests.swift └── Info.plist ├── ModuleArchitectureDemoUITests ├── APIClientArchitectureDemoUITests.swift └── Info.plist ├── README.md └── Text.rtf /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 poksi592 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 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E3FE53B20A9BADC000C3783 /* ApplicationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3FE53A20A9BADC000C3783 /* ApplicationRouter.swift */; }; 11 | 3E3FE54020B00CAA000C3783 /* WireframeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3FE53F20B00CAA000C3783 /* WireframeType.swift */; }; 12 | 3E3FE57D20B433AB000C3783 /* PaymentsStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E3FE57C20B433AB000C3783 /* PaymentsStoryboard.storyboard */; }; 13 | 3E3FE57F20B44DF2000C3783 /* PaymentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3FE57E20B44DF2000C3783 /* PaymentsViewController.swift */; }; 14 | 3E3FE58320B45399000C3783 /* VCExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3FE58220B45399000C3783 /* VCExtensions.swift */; }; 15 | 3E5987702061879F0024F7F7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E59876F2061879F0024F7F7 /* AppDelegate.swift */; }; 16 | 3E5987722061879F0024F7F7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5987712061879F0024F7F7 /* ViewController.swift */; }; 17 | 3E5987752061879F0024F7F7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E5987732061879F0024F7F7 /* Main.storyboard */; }; 18 | 3E5987772061879F0024F7F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E5987762061879F0024F7F7 /* Assets.xcassets */; }; 19 | 3E59877A2061879F0024F7F7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E5987782061879F0024F7F7 /* LaunchScreen.storyboard */; }; 20 | 3E5987852061879F0024F7F7 /* APIClientArchitectureDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5987842061879F0024F7F7 /* APIClientArchitectureDemoTests.swift */; }; 21 | 3E5987902061879F0024F7F7 /* APIClientArchitectureDemoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E59878F2061879F0024F7F7 /* APIClientArchitectureDemoUITests.swift */; }; 22 | 3E5987A52061889B0024F7F7 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5987A42061889B0024F7F7 /* Module.swift */; }; 23 | 3E5987A820618A6C0024F7F7 /* LoginModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5987A720618A6C0024F7F7 /* LoginModule.swift */; }; 24 | 3E5DD5A4209D9D9E00B556C0 /* NonConformingModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5DD5A3209D9D9E00B556C0 /* NonConformingModule.swift */; }; 25 | 3E6F83E12065756F004AF262 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6F83E02065756F004AF262 /* URLExtensions.swift */; }; 26 | 3E6F83E32066AA6E004AF262 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6F83E22066AA6E004AF262 /* Bundle.swift */; }; 27 | 3E6F83E52068C5D3004AF262 /* PaymentsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6F83E42068C5D3004AF262 /* PaymentsModule.swift */; }; 28 | 3E6F83E7206958F7004AF262 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6F83E6206958F7004AF262 /* NetworkService.swift */; }; 29 | CC281D8B20E284D700CE2673 /* PaymentsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D8A20E284D700CE2673 /* PaymentsInteractor.swift */; }; 30 | CC281D8F20E3935C00CE2673 /* WeakContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D8E20E3935C00CE2673 /* WeakContainer.swift */; }; 31 | CC281D9120E4C9CF00CE2673 /* ApplicationServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9020E4C9CF00CE2673 /* ApplicationServices.swift */; }; 32 | CC281D9320E524C300CE2673 /* PaymentsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9220E524C300CE2673 /* PaymentsPresenter.swift */; }; 33 | CC281D9520E635A600CE2673 /* LoginStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC281D9420E635A600CE2673 /* LoginStoryboard.storyboard */; }; 34 | CC281D9920E635FD00CE2673 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9820E635FD00CE2673 /* LoginInteractor.swift */; }; 35 | CC281D9B20E6360A00CE2673 /* LoginPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9A20E6360A00CE2673 /* LoginPresenter.swift */; }; 36 | CC281D9D20E63A4600CE2673 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9C20E63A4600CE2673 /* LoginViewController.swift */; }; 37 | CC281D9F20E6428400CE2673 /* PaymentsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC281D9E20E6428400CE2673 /* PaymentsWireframe.swift */; }; 38 | CC97C6AA20E6862300B806AC /* LoginWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC97C6A920E6862200B806AC /* LoginWireframe.swift */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | 3E5987812061879F0024F7F7 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 3E5987642061879F0024F7F7 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 3E59876B2061879F0024F7F7; 47 | remoteInfo = APIClientArchitectureDemo; 48 | }; 49 | 3E59878C2061879F0024F7F7 /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = 3E5987642061879F0024F7F7 /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = 3E59876B2061879F0024F7F7; 54 | remoteInfo = APIClientArchitectureDemo; 55 | }; 56 | /* End PBXContainerItemProxy section */ 57 | 58 | /* Begin PBXFileReference section */ 59 | 3E3FE53A20A9BADC000C3783 /* ApplicationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouter.swift; sourceTree = ""; }; 60 | 3E3FE53F20B00CAA000C3783 /* WireframeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireframeType.swift; sourceTree = ""; }; 61 | 3E3FE57C20B433AB000C3783 /* PaymentsStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PaymentsStoryboard.storyboard; sourceTree = ""; }; 62 | 3E3FE57E20B44DF2000C3783 /* PaymentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsViewController.swift; sourceTree = ""; }; 63 | 3E3FE58220B45399000C3783 /* VCExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCExtensions.swift; sourceTree = ""; }; 64 | 3E59876C2061879F0024F7F7 /* ModuleArchitectureDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModuleArchitectureDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 3E59876F2061879F0024F7F7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 66 | 3E5987712061879F0024F7F7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 67 | 3E5987742061879F0024F7F7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 68 | 3E5987762061879F0024F7F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 69 | 3E5987792061879F0024F7F7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 70 | 3E59877B2061879F0024F7F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 3E5987802061879F0024F7F7 /* ModuleArchitectureDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ModuleArchitectureDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 3E5987842061879F0024F7F7 /* APIClientArchitectureDemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientArchitectureDemoTests.swift; sourceTree = ""; }; 73 | 3E5987862061879F0024F7F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 3E59878B2061879F0024F7F7 /* ModuleArchitectureDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ModuleArchitectureDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 3E59878F2061879F0024F7F7 /* APIClientArchitectureDemoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientArchitectureDemoUITests.swift; sourceTree = ""; }; 76 | 3E5987912061879F0024F7F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77 | 3E5987A42061889B0024F7F7 /* Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; 78 | 3E5987A720618A6C0024F7F7 /* LoginModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModule.swift; sourceTree = ""; }; 79 | 3E5DD5A3209D9D9E00B556C0 /* NonConformingModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonConformingModule.swift; sourceTree = ""; }; 80 | 3E6F83E02065756F004AF262 /* URLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; 81 | 3E6F83E22066AA6E004AF262 /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 82 | 3E6F83E42068C5D3004AF262 /* PaymentsModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsModule.swift; sourceTree = ""; }; 83 | 3E6F83E6206958F7004AF262 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; 84 | CC281D8A20E284D700CE2673 /* PaymentsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsInteractor.swift; sourceTree = ""; }; 85 | CC281D8E20E3935C00CE2673 /* WeakContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakContainer.swift; sourceTree = ""; }; 86 | CC281D9020E4C9CF00CE2673 /* ApplicationServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationServices.swift; sourceTree = ""; }; 87 | CC281D9220E524C300CE2673 /* PaymentsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsPresenter.swift; sourceTree = ""; }; 88 | CC281D9420E635A600CE2673 /* LoginStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LoginStoryboard.storyboard; sourceTree = ""; }; 89 | CC281D9820E635FD00CE2673 /* LoginInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = ""; }; 90 | CC281D9A20E6360A00CE2673 /* LoginPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPresenter.swift; sourceTree = ""; }; 91 | CC281D9C20E63A4600CE2673 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 92 | CC281D9E20E6428400CE2673 /* PaymentsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsWireframe.swift; sourceTree = ""; }; 93 | CC97C6A920E6862200B806AC /* LoginWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginWireframe.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | 3E5987692061879F0024F7F7 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | 3E59877D2061879F0024F7F7 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | 3E5987882061879F0024F7F7 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | /* End PBXFrameworksBuildPhase section */ 119 | 120 | /* Begin PBXGroup section */ 121 | 3E3FE54120B36435000C3783 /* Storyboards */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 3E3FE57C20B433AB000C3783 /* PaymentsStoryboard.storyboard */, 125 | CC281D9420E635A600CE2673 /* LoginStoryboard.storyboard */, 126 | ); 127 | path = Storyboards; 128 | sourceTree = ""; 129 | }; 130 | 3E3FE58020B44E1E000C3783 /* Payments */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 3E6F83E42068C5D3004AF262 /* PaymentsModule.swift */, 134 | 3E3FE57E20B44DF2000C3783 /* PaymentsViewController.swift */, 135 | CC281D8A20E284D700CE2673 /* PaymentsInteractor.swift */, 136 | CC281D9220E524C300CE2673 /* PaymentsPresenter.swift */, 137 | CC281D9E20E6428400CE2673 /* PaymentsWireframe.swift */, 138 | ); 139 | path = Payments; 140 | sourceTree = ""; 141 | }; 142 | 3E5987632061879F0024F7F7 = { 143 | isa = PBXGroup; 144 | children = ( 145 | 3E59876E2061879F0024F7F7 /* ModuleArchitectureDemo */, 146 | 3E5987832061879F0024F7F7 /* ModuleArchitectureDemoTests */, 147 | 3E59878E2061879F0024F7F7 /* ModuleArchitectureDemoUITests */, 148 | 3E59876D2061879F0024F7F7 /* Products */, 149 | ); 150 | sourceTree = ""; 151 | }; 152 | 3E59876D2061879F0024F7F7 /* Products */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 3E59876C2061879F0024F7F7 /* ModuleArchitectureDemo.app */, 156 | 3E5987802061879F0024F7F7 /* ModuleArchitectureDemoTests.xctest */, 157 | 3E59878B2061879F0024F7F7 /* ModuleArchitectureDemoUITests.xctest */, 158 | ); 159 | name = Products; 160 | sourceTree = ""; 161 | }; 162 | 3E59876E2061879F0024F7F7 /* ModuleArchitectureDemo */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 3E3FE54120B36435000C3783 /* Storyboards */, 166 | 3E6F83DF20657527004AF262 /* Helpers */, 167 | 3E5987A620618A290024F7F7 /* Modules */, 168 | 3E5987A3206188700024F7F7 /* Routing */, 169 | 3E59876F2061879F0024F7F7 /* AppDelegate.swift */, 170 | 3E5987712061879F0024F7F7 /* ViewController.swift */, 171 | 3E5987732061879F0024F7F7 /* Main.storyboard */, 172 | 3E5987762061879F0024F7F7 /* Assets.xcassets */, 173 | 3E5987782061879F0024F7F7 /* LaunchScreen.storyboard */, 174 | 3E59877B2061879F0024F7F7 /* Info.plist */, 175 | 3E6F83E6206958F7004AF262 /* NetworkService.swift */, 176 | ); 177 | path = ModuleArchitectureDemo; 178 | sourceTree = ""; 179 | }; 180 | 3E5987832061879F0024F7F7 /* ModuleArchitectureDemoTests */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 3E5987842061879F0024F7F7 /* APIClientArchitectureDemoTests.swift */, 184 | 3E5987862061879F0024F7F7 /* Info.plist */, 185 | ); 186 | path = ModuleArchitectureDemoTests; 187 | sourceTree = ""; 188 | }; 189 | 3E59878E2061879F0024F7F7 /* ModuleArchitectureDemoUITests */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 3E59878F2061879F0024F7F7 /* APIClientArchitectureDemoUITests.swift */, 193 | 3E5987912061879F0024F7F7 /* Info.plist */, 194 | ); 195 | path = ModuleArchitectureDemoUITests; 196 | sourceTree = ""; 197 | }; 198 | 3E5987A3206188700024F7F7 /* Routing */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 3E5987A42061889B0024F7F7 /* Module.swift */, 202 | 3E3FE53A20A9BADC000C3783 /* ApplicationRouter.swift */, 203 | 3E3FE53F20B00CAA000C3783 /* WireframeType.swift */, 204 | CC281D9020E4C9CF00CE2673 /* ApplicationServices.swift */, 205 | ); 206 | path = Routing; 207 | sourceTree = ""; 208 | }; 209 | 3E5987A620618A290024F7F7 /* Modules */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | CC281D9720E635EB00CE2673 /* Login */, 213 | 3E3FE58020B44E1E000C3783 /* Payments */, 214 | 3E5DD5A3209D9D9E00B556C0 /* NonConformingModule.swift */, 215 | ); 216 | path = Modules; 217 | sourceTree = ""; 218 | }; 219 | 3E6F83DF20657527004AF262 /* Helpers */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 3E6F83E22066AA6E004AF262 /* Bundle.swift */, 223 | 3E6F83E02065756F004AF262 /* URLExtensions.swift */, 224 | 3E3FE58220B45399000C3783 /* VCExtensions.swift */, 225 | CC281D8E20E3935C00CE2673 /* WeakContainer.swift */, 226 | ); 227 | path = Helpers; 228 | sourceTree = ""; 229 | }; 230 | CC281D9720E635EB00CE2673 /* Login */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | 3E5987A720618A6C0024F7F7 /* LoginModule.swift */, 234 | CC281D9820E635FD00CE2673 /* LoginInteractor.swift */, 235 | CC281D9A20E6360A00CE2673 /* LoginPresenter.swift */, 236 | CC281D9C20E63A4600CE2673 /* LoginViewController.swift */, 237 | CC97C6A920E6862200B806AC /* LoginWireframe.swift */, 238 | ); 239 | path = Login; 240 | sourceTree = ""; 241 | }; 242 | /* End PBXGroup section */ 243 | 244 | /* Begin PBXNativeTarget section */ 245 | 3E59876B2061879F0024F7F7 /* ModuleArchitectureDemo */ = { 246 | isa = PBXNativeTarget; 247 | buildConfigurationList = 3E5987942061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemo" */; 248 | buildPhases = ( 249 | 3E5987682061879F0024F7F7 /* Sources */, 250 | 3E5987692061879F0024F7F7 /* Frameworks */, 251 | 3E59876A2061879F0024F7F7 /* Resources */, 252 | ); 253 | buildRules = ( 254 | ); 255 | dependencies = ( 256 | ); 257 | name = ModuleArchitectureDemo; 258 | productName = APIClientArchitectureDemo; 259 | productReference = 3E59876C2061879F0024F7F7 /* ModuleArchitectureDemo.app */; 260 | productType = "com.apple.product-type.application"; 261 | }; 262 | 3E59877F2061879F0024F7F7 /* ModuleArchitectureDemoTests */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 3E5987972061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemoTests" */; 265 | buildPhases = ( 266 | 3E59877C2061879F0024F7F7 /* Sources */, 267 | 3E59877D2061879F0024F7F7 /* Frameworks */, 268 | 3E59877E2061879F0024F7F7 /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | 3E5987822061879F0024F7F7 /* PBXTargetDependency */, 274 | ); 275 | name = ModuleArchitectureDemoTests; 276 | productName = APIClientArchitectureDemoTests; 277 | productReference = 3E5987802061879F0024F7F7 /* ModuleArchitectureDemoTests.xctest */; 278 | productType = "com.apple.product-type.bundle.unit-test"; 279 | }; 280 | 3E59878A2061879F0024F7F7 /* ModuleArchitectureDemoUITests */ = { 281 | isa = PBXNativeTarget; 282 | buildConfigurationList = 3E59879A2061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemoUITests" */; 283 | buildPhases = ( 284 | 3E5987872061879F0024F7F7 /* Sources */, 285 | 3E5987882061879F0024F7F7 /* Frameworks */, 286 | 3E5987892061879F0024F7F7 /* Resources */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | 3E59878D2061879F0024F7F7 /* PBXTargetDependency */, 292 | ); 293 | name = ModuleArchitectureDemoUITests; 294 | productName = APIClientArchitectureDemoUITests; 295 | productReference = 3E59878B2061879F0024F7F7 /* ModuleArchitectureDemoUITests.xctest */; 296 | productType = "com.apple.product-type.bundle.ui-testing"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | 3E5987642061879F0024F7F7 /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | LastSwiftUpdateCheck = 0920; 305 | LastUpgradeCheck = 0930; 306 | ORGANIZATIONNAME = "Mladen Despotovic"; 307 | TargetAttributes = { 308 | 3E59876B2061879F0024F7F7 = { 309 | CreatedOnToolsVersion = 9.2; 310 | ProvisioningStyle = Automatic; 311 | }; 312 | 3E59877F2061879F0024F7F7 = { 313 | CreatedOnToolsVersion = 9.2; 314 | ProvisioningStyle = Automatic; 315 | TestTargetID = 3E59876B2061879F0024F7F7; 316 | }; 317 | 3E59878A2061879F0024F7F7 = { 318 | CreatedOnToolsVersion = 9.2; 319 | ProvisioningStyle = Automatic; 320 | TestTargetID = 3E59876B2061879F0024F7F7; 321 | }; 322 | }; 323 | }; 324 | buildConfigurationList = 3E5987672061879F0024F7F7 /* Build configuration list for PBXProject "ModuleArchitectureDemo" */; 325 | compatibilityVersion = "Xcode 8.0"; 326 | developmentRegion = en; 327 | hasScannedForEncodings = 0; 328 | knownRegions = ( 329 | en, 330 | Base, 331 | ); 332 | mainGroup = 3E5987632061879F0024F7F7; 333 | productRefGroup = 3E59876D2061879F0024F7F7 /* Products */; 334 | projectDirPath = ""; 335 | projectRoot = ""; 336 | targets = ( 337 | 3E59876B2061879F0024F7F7 /* ModuleArchitectureDemo */, 338 | 3E59877F2061879F0024F7F7 /* ModuleArchitectureDemoTests */, 339 | 3E59878A2061879F0024F7F7 /* ModuleArchitectureDemoUITests */, 340 | ); 341 | }; 342 | /* End PBXProject section */ 343 | 344 | /* Begin PBXResourcesBuildPhase section */ 345 | 3E59876A2061879F0024F7F7 /* Resources */ = { 346 | isa = PBXResourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | 3E59877A2061879F0024F7F7 /* LaunchScreen.storyboard in Resources */, 350 | CC281D9520E635A600CE2673 /* LoginStoryboard.storyboard in Resources */, 351 | 3E5987772061879F0024F7F7 /* Assets.xcassets in Resources */, 352 | 3E3FE57D20B433AB000C3783 /* PaymentsStoryboard.storyboard in Resources */, 353 | 3E5987752061879F0024F7F7 /* Main.storyboard in Resources */, 354 | ); 355 | runOnlyForDeploymentPostprocessing = 0; 356 | }; 357 | 3E59877E2061879F0024F7F7 /* Resources */ = { 358 | isa = PBXResourcesBuildPhase; 359 | buildActionMask = 2147483647; 360 | files = ( 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | }; 364 | 3E5987892061879F0024F7F7 /* Resources */ = { 365 | isa = PBXResourcesBuildPhase; 366 | buildActionMask = 2147483647; 367 | files = ( 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | }; 371 | /* End PBXResourcesBuildPhase section */ 372 | 373 | /* Begin PBXSourcesBuildPhase section */ 374 | 3E5987682061879F0024F7F7 /* Sources */ = { 375 | isa = PBXSourcesBuildPhase; 376 | buildActionMask = 2147483647; 377 | files = ( 378 | CC97C6AA20E6862300B806AC /* LoginWireframe.swift in Sources */, 379 | 3E5987A52061889B0024F7F7 /* Module.swift in Sources */, 380 | 3E3FE53B20A9BADC000C3783 /* ApplicationRouter.swift in Sources */, 381 | 3E6F83E32066AA6E004AF262 /* Bundle.swift in Sources */, 382 | 3E3FE58320B45399000C3783 /* VCExtensions.swift in Sources */, 383 | CC281D9B20E6360A00CE2673 /* LoginPresenter.swift in Sources */, 384 | 3E3FE57F20B44DF2000C3783 /* PaymentsViewController.swift in Sources */, 385 | 3E6F83E7206958F7004AF262 /* NetworkService.swift in Sources */, 386 | 3E5987722061879F0024F7F7 /* ViewController.swift in Sources */, 387 | CC281D9D20E63A4600CE2673 /* LoginViewController.swift in Sources */, 388 | CC281D8B20E284D700CE2673 /* PaymentsInteractor.swift in Sources */, 389 | CC281D9920E635FD00CE2673 /* LoginInteractor.swift in Sources */, 390 | 3E3FE54020B00CAA000C3783 /* WireframeType.swift in Sources */, 391 | CC281D9320E524C300CE2673 /* PaymentsPresenter.swift in Sources */, 392 | 3E6F83E12065756F004AF262 /* URLExtensions.swift in Sources */, 393 | 3E5DD5A4209D9D9E00B556C0 /* NonConformingModule.swift in Sources */, 394 | CC281D8F20E3935C00CE2673 /* WeakContainer.swift in Sources */, 395 | CC281D9120E4C9CF00CE2673 /* ApplicationServices.swift in Sources */, 396 | 3E5987702061879F0024F7F7 /* AppDelegate.swift in Sources */, 397 | CC281D9F20E6428400CE2673 /* PaymentsWireframe.swift in Sources */, 398 | 3E6F83E52068C5D3004AF262 /* PaymentsModule.swift in Sources */, 399 | 3E5987A820618A6C0024F7F7 /* LoginModule.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 3E59877C2061879F0024F7F7 /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 3E5987852061879F0024F7F7 /* APIClientArchitectureDemoTests.swift in Sources */, 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | 3E5987872061879F0024F7F7 /* Sources */ = { 412 | isa = PBXSourcesBuildPhase; 413 | buildActionMask = 2147483647; 414 | files = ( 415 | 3E5987902061879F0024F7F7 /* APIClientArchitectureDemoUITests.swift in Sources */, 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | /* End PBXSourcesBuildPhase section */ 420 | 421 | /* Begin PBXTargetDependency section */ 422 | 3E5987822061879F0024F7F7 /* PBXTargetDependency */ = { 423 | isa = PBXTargetDependency; 424 | target = 3E59876B2061879F0024F7F7 /* ModuleArchitectureDemo */; 425 | targetProxy = 3E5987812061879F0024F7F7 /* PBXContainerItemProxy */; 426 | }; 427 | 3E59878D2061879F0024F7F7 /* PBXTargetDependency */ = { 428 | isa = PBXTargetDependency; 429 | target = 3E59876B2061879F0024F7F7 /* ModuleArchitectureDemo */; 430 | targetProxy = 3E59878C2061879F0024F7F7 /* PBXContainerItemProxy */; 431 | }; 432 | /* End PBXTargetDependency section */ 433 | 434 | /* Begin PBXVariantGroup section */ 435 | 3E5987732061879F0024F7F7 /* Main.storyboard */ = { 436 | isa = PBXVariantGroup; 437 | children = ( 438 | 3E5987742061879F0024F7F7 /* Base */, 439 | ); 440 | name = Main.storyboard; 441 | sourceTree = ""; 442 | }; 443 | 3E5987782061879F0024F7F7 /* LaunchScreen.storyboard */ = { 444 | isa = PBXVariantGroup; 445 | children = ( 446 | 3E5987792061879F0024F7F7 /* Base */, 447 | ); 448 | name = LaunchScreen.storyboard; 449 | sourceTree = ""; 450 | }; 451 | /* End PBXVariantGroup section */ 452 | 453 | /* Begin XCBuildConfiguration section */ 454 | 3E5987922061879F0024F7F7 /* Debug */ = { 455 | isa = XCBuildConfiguration; 456 | buildSettings = { 457 | ALWAYS_SEARCH_USER_PATHS = NO; 458 | CLANG_ANALYZER_NONNULL = YES; 459 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 460 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 461 | CLANG_CXX_LIBRARY = "libc++"; 462 | CLANG_ENABLE_MODULES = YES; 463 | CLANG_ENABLE_OBJC_ARC = YES; 464 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 465 | CLANG_WARN_BOOL_CONVERSION = YES; 466 | CLANG_WARN_COMMA = YES; 467 | CLANG_WARN_CONSTANT_CONVERSION = YES; 468 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 470 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 471 | CLANG_WARN_EMPTY_BODY = YES; 472 | CLANG_WARN_ENUM_CONVERSION = YES; 473 | CLANG_WARN_INFINITE_RECURSION = YES; 474 | CLANG_WARN_INT_CONVERSION = YES; 475 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 476 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 477 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 478 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 479 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 480 | CLANG_WARN_STRICT_PROTOTYPES = YES; 481 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 482 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 483 | CLANG_WARN_UNREACHABLE_CODE = YES; 484 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 485 | CODE_SIGN_IDENTITY = "iPhone Developer"; 486 | COPY_PHASE_STRIP = NO; 487 | DEBUG_INFORMATION_FORMAT = dwarf; 488 | ENABLE_STRICT_OBJC_MSGSEND = YES; 489 | ENABLE_TESTABILITY = YES; 490 | GCC_C_LANGUAGE_STANDARD = gnu11; 491 | GCC_DYNAMIC_NO_PIC = NO; 492 | GCC_NO_COMMON_BLOCKS = YES; 493 | GCC_OPTIMIZATION_LEVEL = 0; 494 | GCC_PREPROCESSOR_DEFINITIONS = ( 495 | "DEBUG=1", 496 | "$(inherited)", 497 | ); 498 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 499 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 500 | GCC_WARN_UNDECLARED_SELECTOR = YES; 501 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 502 | GCC_WARN_UNUSED_FUNCTION = YES; 503 | GCC_WARN_UNUSED_VARIABLE = YES; 504 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 505 | MTL_ENABLE_DEBUG_INFO = YES; 506 | ONLY_ACTIVE_ARCH = YES; 507 | SDKROOT = iphoneos; 508 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 510 | }; 511 | name = Debug; 512 | }; 513 | 3E5987932061879F0024F7F7 /* Release */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ALWAYS_SEARCH_USER_PATHS = NO; 517 | CLANG_ANALYZER_NONNULL = YES; 518 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 519 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 520 | CLANG_CXX_LIBRARY = "libc++"; 521 | CLANG_ENABLE_MODULES = YES; 522 | CLANG_ENABLE_OBJC_ARC = YES; 523 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 524 | CLANG_WARN_BOOL_CONVERSION = YES; 525 | CLANG_WARN_COMMA = YES; 526 | CLANG_WARN_CONSTANT_CONVERSION = YES; 527 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 528 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 529 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 530 | CLANG_WARN_EMPTY_BODY = YES; 531 | CLANG_WARN_ENUM_CONVERSION = YES; 532 | CLANG_WARN_INFINITE_RECURSION = YES; 533 | CLANG_WARN_INT_CONVERSION = YES; 534 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 535 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 536 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 537 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 538 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 539 | CLANG_WARN_STRICT_PROTOTYPES = YES; 540 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 541 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 542 | CLANG_WARN_UNREACHABLE_CODE = YES; 543 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 544 | CODE_SIGN_IDENTITY = "iPhone Developer"; 545 | COPY_PHASE_STRIP = NO; 546 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 547 | ENABLE_NS_ASSERTIONS = NO; 548 | ENABLE_STRICT_OBJC_MSGSEND = YES; 549 | GCC_C_LANGUAGE_STANDARD = gnu11; 550 | GCC_NO_COMMON_BLOCKS = YES; 551 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 552 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 553 | GCC_WARN_UNDECLARED_SELECTOR = YES; 554 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 555 | GCC_WARN_UNUSED_FUNCTION = YES; 556 | GCC_WARN_UNUSED_VARIABLE = YES; 557 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 558 | MTL_ENABLE_DEBUG_INFO = NO; 559 | SDKROOT = iphoneos; 560 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 561 | VALIDATE_PRODUCT = YES; 562 | }; 563 | name = Release; 564 | }; 565 | 3E5987952061879F0024F7F7 /* Debug */ = { 566 | isa = XCBuildConfiguration; 567 | buildSettings = { 568 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 569 | CODE_SIGN_STYLE = Automatic; 570 | DEVELOPMENT_TEAM = JP9LNGT4C2; 571 | INFOPLIST_FILE = ModuleArchitectureDemo/Info.plist; 572 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 573 | PRODUCT_BUNDLE_IDENTIFIER = mdc.ModuleArchitectureDemo; 574 | PRODUCT_NAME = "$(TARGET_NAME)"; 575 | SWIFT_VERSION = 4.0; 576 | TARGETED_DEVICE_FAMILY = "1,2"; 577 | }; 578 | name = Debug; 579 | }; 580 | 3E5987962061879F0024F7F7 /* Release */ = { 581 | isa = XCBuildConfiguration; 582 | buildSettings = { 583 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 584 | CODE_SIGN_STYLE = Automatic; 585 | DEVELOPMENT_TEAM = JP9LNGT4C2; 586 | INFOPLIST_FILE = ModuleArchitectureDemo/Info.plist; 587 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 588 | PRODUCT_BUNDLE_IDENTIFIER = mdc.ModuleArchitectureDemo; 589 | PRODUCT_NAME = "$(TARGET_NAME)"; 590 | SWIFT_VERSION = 4.0; 591 | TARGETED_DEVICE_FAMILY = "1,2"; 592 | }; 593 | name = Release; 594 | }; 595 | 3E5987982061879F0024F7F7 /* Debug */ = { 596 | isa = XCBuildConfiguration; 597 | buildSettings = { 598 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 599 | BUNDLE_LOADER = "$(TEST_HOST)"; 600 | CODE_SIGN_STYLE = Automatic; 601 | DEVELOPMENT_TEAM = JP9LNGT4C2; 602 | INFOPLIST_FILE = APIClientArchitectureDemoTests/Info.plist; 603 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 604 | PRODUCT_BUNDLE_IDENTIFIER = mdc.APIClientArchitectureDemoTests; 605 | PRODUCT_NAME = "$(TARGET_NAME)"; 606 | SWIFT_VERSION = 4.0; 607 | TARGETED_DEVICE_FAMILY = "1,2"; 608 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModuleArchitectureDemo.app/ModuleArchitectureDemo"; 609 | }; 610 | name = Debug; 611 | }; 612 | 3E5987992061879F0024F7F7 /* Release */ = { 613 | isa = XCBuildConfiguration; 614 | buildSettings = { 615 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 616 | BUNDLE_LOADER = "$(TEST_HOST)"; 617 | CODE_SIGN_STYLE = Automatic; 618 | DEVELOPMENT_TEAM = JP9LNGT4C2; 619 | INFOPLIST_FILE = APIClientArchitectureDemoTests/Info.plist; 620 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 621 | PRODUCT_BUNDLE_IDENTIFIER = mdc.APIClientArchitectureDemoTests; 622 | PRODUCT_NAME = "$(TARGET_NAME)"; 623 | SWIFT_VERSION = 4.0; 624 | TARGETED_DEVICE_FAMILY = "1,2"; 625 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModuleArchitectureDemo.app/ModuleArchitectureDemo"; 626 | }; 627 | name = Release; 628 | }; 629 | 3E59879B2061879F0024F7F7 /* Debug */ = { 630 | isa = XCBuildConfiguration; 631 | buildSettings = { 632 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 633 | CODE_SIGN_STYLE = Automatic; 634 | DEVELOPMENT_TEAM = JP9LNGT4C2; 635 | INFOPLIST_FILE = APIClientArchitectureDemoUITests/Info.plist; 636 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 637 | PRODUCT_BUNDLE_IDENTIFIER = mdc.APIClientArchitectureDemoUITests; 638 | PRODUCT_NAME = "$(TARGET_NAME)"; 639 | SWIFT_VERSION = 4.0; 640 | TARGETED_DEVICE_FAMILY = "1,2"; 641 | TEST_TARGET_NAME = APIClientArchitectureDemo; 642 | }; 643 | name = Debug; 644 | }; 645 | 3E59879C2061879F0024F7F7 /* Release */ = { 646 | isa = XCBuildConfiguration; 647 | buildSettings = { 648 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 649 | CODE_SIGN_STYLE = Automatic; 650 | DEVELOPMENT_TEAM = JP9LNGT4C2; 651 | INFOPLIST_FILE = APIClientArchitectureDemoUITests/Info.plist; 652 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 653 | PRODUCT_BUNDLE_IDENTIFIER = mdc.APIClientArchitectureDemoUITests; 654 | PRODUCT_NAME = "$(TARGET_NAME)"; 655 | SWIFT_VERSION = 4.0; 656 | TARGETED_DEVICE_FAMILY = "1,2"; 657 | TEST_TARGET_NAME = APIClientArchitectureDemo; 658 | }; 659 | name = Release; 660 | }; 661 | /* End XCBuildConfiguration section */ 662 | 663 | /* Begin XCConfigurationList section */ 664 | 3E5987672061879F0024F7F7 /* Build configuration list for PBXProject "ModuleArchitectureDemo" */ = { 665 | isa = XCConfigurationList; 666 | buildConfigurations = ( 667 | 3E5987922061879F0024F7F7 /* Debug */, 668 | 3E5987932061879F0024F7F7 /* Release */, 669 | ); 670 | defaultConfigurationIsVisible = 0; 671 | defaultConfigurationName = Release; 672 | }; 673 | 3E5987942061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemo" */ = { 674 | isa = XCConfigurationList; 675 | buildConfigurations = ( 676 | 3E5987952061879F0024F7F7 /* Debug */, 677 | 3E5987962061879F0024F7F7 /* Release */, 678 | ); 679 | defaultConfigurationIsVisible = 0; 680 | defaultConfigurationName = Release; 681 | }; 682 | 3E5987972061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemoTests" */ = { 683 | isa = XCConfigurationList; 684 | buildConfigurations = ( 685 | 3E5987982061879F0024F7F7 /* Debug */, 686 | 3E5987992061879F0024F7F7 /* Release */, 687 | ); 688 | defaultConfigurationIsVisible = 0; 689 | defaultConfigurationName = Release; 690 | }; 691 | 3E59879A2061879F0024F7F7 /* Build configuration list for PBXNativeTarget "ModuleArchitectureDemoUITests" */ = { 692 | isa = XCConfigurationList; 693 | buildConfigurations = ( 694 | 3E59879B2061879F0024F7F7 /* Debug */, 695 | 3E59879C2061879F0024F7F7 /* Release */, 696 | ); 697 | defaultConfigurationIsVisible = 0; 698 | defaultConfigurationName = Release; 699 | }; 700 | /* End XCConfigurationList section */ 701 | }; 702 | rootObject = 3E5987642061879F0024F7F7 /* Project object */; 703 | } 704 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // APIClientArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 20/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /ModuleArchitectureDemo/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 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Helpers/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 14/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | 13 | var urlSchemes: [String]? { 14 | 15 | get { 16 | guard let urlTypes = self.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: AnyObject]] else { 17 | return nil 18 | } 19 | let urlSchemes = urlTypes.compactMap { (item) -> [String]? in 20 | 21 | guard let schemes = item["CFBundleURLSchemes"] as? [String] else { 22 | 23 | return nil 24 | } 25 | return schemes 26 | } 27 | return urlSchemes.flatMap { $0 } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Helpers/URLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLExtensions.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 14/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | 13 | init?(schema: String, 14 | host: String, 15 | path: String? = nil, 16 | parameters: [String: String]? = nil) { 17 | 18 | var components = URLComponents() 19 | components.scheme = schema 20 | components.host = host 21 | components.path = path ?? "" 22 | 23 | let queryItems = parameters?.map { key, value -> URLQueryItem in 24 | 25 | return URLQueryItem.init(name: key, value: value) 26 | } 27 | components.queryItems = queryItems 28 | 29 | if let url = components.url { 30 | self = url 31 | } 32 | else { 33 | return nil 34 | } 35 | } 36 | 37 | /** 38 | This extension uses resource from URL in main bundle and converts it to JSON dictionary. 39 | 40 | - returns: `[String: Any]?` 41 | */ 42 | func jsonFromMainBundle() -> [String: Any]? { 43 | 44 | do { 45 | let data = try Data(contentsOf: self) 46 | let json = try JSONSerialization.jsonObject(with: data, options: []) 47 | return json as? [String: Any] 48 | } catch { 49 | return nil 50 | } 51 | } 52 | 53 | func containsInAppSchema(for bundle: Bundle? = Bundle.main) -> Bool { 54 | 55 | guard let schemes = bundle?.urlSchemes else { 56 | return false 57 | } 58 | guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), 59 | let scheme = components.scheme else { 60 | 61 | return false 62 | } 63 | return schemes.filter { $0 == scheme }.count > 0 64 | } 65 | 66 | var isHttpAddress: Bool { 67 | 68 | get { 69 | if let components = URLComponents(url: self, resolvingAgainstBaseURL: false), 70 | let scheme = components.scheme, 71 | let _ = components.host, 72 | scheme == "http" || scheme == "https" { 73 | 74 | return true 75 | } 76 | else { 77 | return false 78 | } 79 | } 80 | } 81 | } 82 | 83 | extension URLComponents { 84 | 85 | var queryItemsDictionary: [String: String]? { 86 | var params = [String: String]() 87 | return self.queryItems?.reduce([:], { (_, item) -> [String: String] in 88 | 89 | params[item.name] = item.value 90 | return params 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Helpers/VCExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCExtensions.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 22/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIViewController { 13 | 14 | class func topPresentedController() -> UIViewController? { 15 | 16 | let delegate = UIApplication.shared.delegate as? AppDelegate 17 | guard let rootViewController = delegate?.window?.rootViewController else { 18 | return nil 19 | } 20 | 21 | var topViewController = rootViewController 22 | while let presentedViewController = topViewController.presentedViewController{ 23 | topViewController = presentedViewController 24 | } 25 | return rootViewController 26 | } 27 | 28 | func topPresentedController() -> UIViewController? { 29 | 30 | let delegate = UIApplication.shared.delegate as? AppDelegate 31 | guard let rootViewController = delegate?.window?.rootViewController else { 32 | return nil 33 | } 34 | 35 | var topViewController = rootViewController 36 | while let presentedViewController = topViewController.presentedViewController{ 37 | topViewController = presentedViewController 38 | } 39 | return self 40 | } 41 | 42 | /** 43 | Simplified function to get the topmost UINavigationController. 44 | This can be written in many ways and can reflect the actual app navigation specifics. 45 | */ 46 | func topmostNavigationController() -> UINavigationController? { 47 | 48 | var topRootViewController = self 49 | while let presentedViewController = topRootViewController.presentedViewController{ 50 | topRootViewController = presentedViewController 51 | } 52 | 53 | switch topRootViewController { 54 | case let navigationViewController as UINavigationController: 55 | return navigationViewController 56 | case let tabBarViewController as UITabBarController: 57 | return tabBarViewController.selectedViewController as? UINavigationController 58 | default: 59 | return nil 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Helpers/WeakContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakContainer.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 27.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct WeakContainer { 12 | 13 | private weak var internalValue: AnyObject? 14 | public var value: T? { 15 | get { 16 | return internalValue as? T 17 | } 18 | set { 19 | internalValue = newValue as AnyObject 20 | } 21 | } 22 | 23 | public init(value: T) { 24 | self.value = value 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleURLSchemes 9 | 10 | yourbank 11 | 12 | 13 | 14 | CFBundleDevelopmentRegion 15 | $(DEVELOPMENT_LANGUAGE) 16 | CFBundleExecutable 17 | $(EXECUTABLE_NAME) 18 | CFBundleIdentifier 19 | $(PRODUCT_BUNDLE_IDENTIFIER) 20 | CFBundleInfoDictionaryVersion 21 | 6.0 22 | CFBundleName 23 | $(PRODUCT_NAME) 24 | CFBundlePackageType 25 | APPL 26 | CFBundleShortVersionString 27 | 1.0 28 | CFBundleVersion 29 | 1 30 | LSRequiresIPhoneOS 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Login/LoginInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginInteractor.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 29.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LoginInteractor: ModuleRoutable { 12 | 13 | static func routable() -> ModuleRoutable { 14 | return self.init() 15 | } 16 | 17 | static func getPaths() -> [String] { 18 | return ["/payment-token", 19 | "/login"] 20 | } 21 | 22 | func route(parameters: ModuleParameters?, 23 | path: String?, 24 | callback: ModuleCallback?) { 25 | 26 | switch path { 27 | 28 | case "/payment-token": 29 | 30 | getPaymentToken(parameters: parameters) { (token, urlResponse, error) in 31 | 32 | if let token = token { 33 | 34 | let response = [LoginModuleParameters.paymentToken.rawValue: token] 35 | callback?(response, nil, urlResponse, nil) 36 | } 37 | else { 38 | 39 | // Simplyfication of error response 40 | callback?(nil, nil, nil, ResponseError(error: error, response: urlResponse)) 41 | } 42 | } 43 | 44 | case "/login": 45 | 46 | login(parameters: parameters) { (token, urlResponse, error) in 47 | 48 | if let token = token { 49 | 50 | let response = [LoginModuleParameters.bearerToken.rawValue: token] 51 | callback?(response, nil, urlResponse, nil) 52 | } 53 | else { 54 | 55 | // Simplyfication of error response 56 | callback?(nil, nil, nil, ResponseError(error: error, response: urlResponse)) 57 | } 58 | } 59 | 60 | default: 61 | return 62 | } 63 | 64 | } 65 | 66 | func getPaymentToken(parameters: ModuleParameters?, 67 | completion: @escaping (String?, HTTPURLResponse?, ResponseError?) -> Void) { 68 | 69 | let service = MockLoginNetworkService() 70 | 71 | // If parameters are not passed, then we exit with Bad Request at once 72 | guard let parameters = parameters, 73 | let username = parameters[LoginModuleParameters.username.rawValue], 74 | let password = parameters[LoginModuleParameters.password.rawValue] else { 75 | 76 | let url = URL.init(schema: "yourbank", host: "login") 77 | let response = HTTPURLResponse.init(url: url!, 78 | statusCode: 400, 79 | httpVersion: nil, 80 | headerFields: nil) 81 | completion(nil, response, nil) 82 | return 83 | } 84 | let getTokenParameters = [LoginModuleParameters.username.rawValue: username, 85 | LoginModuleParameters.password.rawValue: password] 86 | service.post(host: "login", 87 | path: "/payment-token", 88 | parameters: getTokenParameters) { (response, urlResponse, error) in 89 | 90 | // We are not going to check errors and URL response status codes, just a shortest path. 91 | var networkError: ResponseError? = nil 92 | if let error = error { 93 | networkError = ResponseError(error: error, response: urlResponse) 94 | } 95 | let token = response?["paymentToken"] as? String 96 | 97 | completion(token, urlResponse, networkError) 98 | } 99 | } 100 | 101 | func login(parameters: ModuleParameters?, 102 | completion: @escaping (String?, HTTPURLResponse?, ResponseError?) -> Void) { 103 | 104 | let service = MockLoginNetworkService() 105 | guard let parameters = parameters, 106 | let username = parameters[LoginModuleParameters.username.rawValue], 107 | let password = parameters[LoginModuleParameters.password.rawValue] else { 108 | return 109 | } 110 | let loginParameters = [LoginModuleParameters.username.rawValue: username, 111 | LoginModuleParameters.password.rawValue: password] 112 | service.post(host: "login", 113 | path: "/login", 114 | parameters: loginParameters) { (response, urlResponse, error) in 115 | 116 | // We are not going to check errors and URL response status codes, just a shortest path. 117 | var networkError: ResponseError? = nil 118 | if let error = error { 119 | networkError = ResponseError(error: error, response: urlResponse) 120 | } 121 | let token = response?["bearerToken"] as? String 122 | 123 | completion(token, urlResponse, networkError) 124 | } 125 | } 126 | } 127 | 128 | class MockLoginNetworkService: NetworkService { 129 | 130 | override func post(scheme: String? = nil, 131 | host: String, 132 | path: String, 133 | parameters: [String : Any]?, 134 | completion: @escaping ([String : Any]?, HTTPURLResponse?, Error?) -> Void) { 135 | 136 | switch path { 137 | case "/payment-token": 138 | 139 | if let parameters = parameters, 140 | parameters[LoginModuleParameters.username.rawValue] as? String == "myUsername", 141 | parameters[LoginModuleParameters.password.rawValue] as? String == "myPassword" { 142 | 143 | let url = URL(schema: "https", 144 | host: host, 145 | path: path, 146 | parameters: parameters as? [String : String]) 147 | 148 | let urlResponse = HTTPURLResponse(url: url!, 149 | statusCode: 200, 150 | httpVersion: nil, 151 | headerFields: nil) 152 | 153 | print("Login for payment token successful, 200") 154 | 155 | completion([LoginModuleParameters.paymentToken.rawValue: "hf120938h12983dh"], urlResponse, nil) 156 | } 157 | else { 158 | 159 | let error = NSError.init(domain: "com.module.architecture.demo.network-errors", 160 | code: 401, 161 | userInfo: nil) 162 | 163 | print("Login for payment token failed, 400") 164 | 165 | completion(nil, nil, error) 166 | } 167 | 168 | case "/login": 169 | 170 | if let parameters = parameters, 171 | parameters[LoginModuleParameters.username.rawValue] as? String == "myUsername", 172 | parameters[LoginModuleParameters.password.rawValue] as? String == "myPassword" { 173 | 174 | let url = URL(schema: "https", 175 | host: host, 176 | path: path, 177 | parameters: parameters as? [String : String]) 178 | 179 | let urlResponse = HTTPURLResponse(url: url!, 180 | statusCode: 200, 181 | httpVersion: nil, 182 | headerFields: nil) 183 | completion([LoginModuleParameters.bearerToken.rawValue: "AbCdEf123456"], urlResponse, nil) 184 | } 185 | else { 186 | 187 | let error = NSError.init(domain: "com.module.architecture.demo.network-errors", 188 | code: 401, 189 | userInfo: nil) 190 | completion(nil, nil, error) 191 | } 192 | 193 | default: 194 | 195 | let error = NSError.init(domain: "com.module.architecture.demo.network-errors", 196 | code: 404, 197 | userInfo: nil) 198 | completion(nil, nil, error) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Login/LoginModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginModule.swift 3 | // APIClientArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 20/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // A simple way of formalising request parameters within a module 12 | enum LoginModuleParameters: String { 13 | 14 | case username 15 | case password 16 | case paymentToken 17 | case bearerToken 18 | } 19 | 20 | class LoginModule: ModuleType { 21 | 22 | func setup(parameters: ModuleParameters?) {} 23 | 24 | var route: String = { 25 | return "login" 26 | }() 27 | 28 | var paths: [String] = { 29 | return ["/login", 30 | "/logout", 31 | "/payment-token"] 32 | }() 33 | 34 | var subscribedRoutables: [ModuleRoutable.Type] = [LoginPresenter.self] 35 | var instantiatedRoutables: [WeakContainer] = [] 36 | } 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Login/LoginPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginPresenter.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 29.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LoginPresenter: ModuleRoutable { 12 | 13 | lazy private var wireframe = LoginWireframe() 14 | lazy private var interactor = LoginInteractor() 15 | private var parameters: ModuleParameters? 16 | private var callback: ModuleCallback? 17 | 18 | static func routable() -> ModuleRoutable { 19 | return self.init() 20 | } 21 | 22 | static func getPaths() -> [String] { 23 | return ["/payment-token", 24 | "/login"] 25 | } 26 | 27 | func route(parameters: ModuleParameters?, path: String?, callback: ModuleCallback?) { 28 | 29 | self.parameters = parameters 30 | self.callback = callback 31 | wireframe.presentLoginViewController(with: self, parameters: parameters) 32 | } 33 | 34 | func login(username: String?, password: String?) { 35 | 36 | guard let username = username, 37 | let password = password else { return } 38 | 39 | parameters?[LoginModuleParameters.username.rawValue] = username 40 | parameters?[LoginModuleParameters.password.rawValue] = password 41 | interactor.getPaymentToken(parameters: parameters) { [weak self] (token, urlResponse, error) in 42 | 43 | let response = [LoginModuleParameters.paymentToken.rawValue: token] 44 | self?.callback?(response, nil, urlResponse, error) 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Login/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 29.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class LoginViewController: StoryboardIdentifiableViewController { 13 | 14 | var presenter: LoginPresenter? 15 | 16 | @IBOutlet weak var usernameField: UITextField? 17 | @IBOutlet weak var passwordField: UITextField? 18 | 19 | @IBAction private func login(_ sender: UIButton) { 20 | 21 | presenter?.login(username: usernameField?.text, password: passwordField?.text) 22 | self.dismiss(animated: true, completion: nil) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Login/LoginWireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginWireframe.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 29.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class LoginWireframe: WireframeType { 13 | 14 | // We use the default storyboard, which can be changed later by injected parameter 15 | lazy var storyboard: UIStoryboard = UIStoryboard(name: "LoginStoryboard", bundle: nil) 16 | lazy var presentedViewControllers = [WeakContainer]() 17 | var presentationMode: ModulePresentationMode = .none 18 | 19 | func presentLoginViewController(with presenter: LoginPresenter, parameters: ModuleParameters?) { 20 | 21 | setPresentationMode(from: parameters) 22 | if let viewController = viewController(from: parameters) { 23 | 24 | present(viewController: viewController) 25 | guard let loginViewController = viewController as? LoginViewController else { return } 26 | loginViewController.presenter = presenter 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/NonConformingModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NonConformingModule.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 05/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class NonConformingModule { 12 | 13 | func login(username: String, 14 | password: String, 15 | completion: ((String?, Error?) -> Void)?) { 16 | 17 | let service = NetworkService() 18 | service.post(scheme: "yourbank", 19 | host: "login", 20 | path: "/login", 21 | parameters: ["username": "myUsername", 22 | "password": "myPassword"]) { (response, urlResponse, error) in 23 | 24 | // We are not going to check errors and URL response status codes, just a shortest path. 25 | var networkError: ResponseError? = nil 26 | if let error = error { 27 | networkError = ResponseError(error: error, response: urlResponse) 28 | } 29 | let token = response?["bearerToken"] as? String 30 | 31 | completion?(token, networkError) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Payments/PaymentsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentsInteractor.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 26.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class PaymentsInteractor { 12 | 13 | func pay(parameters: ModuleParameters?, 14 | completion: @escaping (HTTPURLResponse?, ResponseError?) -> Void) { 15 | 16 | let service = MockPaymentsNetworkService() 17 | guard let parameters = parameters, 18 | let amount = parameters[PaymentsModuleParameters.suggestedAmount.rawValue] else { 19 | return 20 | } 21 | let token = parameters[PaymentsModuleParameters.token.rawValue] 22 | let payParameters = [PaymentsModuleParameters.token.rawValue: token ?? "", 23 | PaymentsModuleParameters.suggestedAmount.rawValue: amount] 24 | 25 | service.post(host: "payments", 26 | path: "/pay", 27 | parameters: payParameters) { (response, urlResponse, error) in 28 | 29 | // We are not going to check errors and URL response status codes, just a shortest path. 30 | var networkingError: ResponseError? = nil 31 | if let error = error { 32 | networkingError = ResponseError(error: error, response: urlResponse) 33 | } 34 | 35 | completion(urlResponse, networkingError) 36 | } 37 | } 38 | } 39 | 40 | class MockPaymentsNetworkService: NetworkService { 41 | 42 | override func post(scheme: String? = nil, 43 | host: String, 44 | path: String, 45 | parameters: [String : Any]?, 46 | completion: @escaping ([String : Any]?, HTTPURLResponse?, Error?) -> Void) { 47 | 48 | // This is a mock service for a specific interactor, where we always expect parameters. 49 | guard let parameters = parameters else { return } 50 | 51 | // If we don't get a specific payment token, then we return 401 52 | if parameters[PaymentsModuleParameters.token.rawValue] as? String != "hf120938h12983dh" { 53 | 54 | let url = URL(schema: "https", 55 | host: host, 56 | path: path, 57 | parameters: parameters as? [String : String]) 58 | let response = HTTPURLResponse.init(url: url!, 59 | statusCode: 401, 60 | httpVersion: nil, 61 | headerFields: nil) 62 | let error = NSError.init(domain: "com.module.architecture.demo.network-errors", 63 | code: 401, 64 | userInfo: nil) 65 | 66 | print("Payment failed, 401") 67 | 68 | completion(nil, response, error) 69 | } 70 | else if parameters[PaymentsModuleParameters.suggestedAmount.rawValue] as? String != "" { 71 | 72 | let url = URL(schema: "https", 73 | host: host, 74 | path: path, 75 | parameters: parameters as? [String : String]) 76 | 77 | let urlResponse = HTTPURLResponse(url: url!, 78 | statusCode: 201, 79 | httpVersion: nil, 80 | headerFields: nil) 81 | 82 | print("Payment successful, 201") 83 | 84 | completion([String: String](), urlResponse, nil) 85 | } 86 | else { 87 | 88 | let error = NSError.init(domain: "com.module.architecture.demo.network-errors", 89 | code: 400, 90 | userInfo: nil) 91 | completion(nil, nil, error) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Payments/PaymentsModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentsModule.swift 3 | // APIClientArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 26/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum PaymentsModuleParameters: String { 12 | 13 | case suggestedAmount 14 | case token 15 | } 16 | 17 | class PaymentModule: ModuleType { 18 | 19 | var route: String = { 20 | return "payments" 21 | }() 22 | 23 | var paths: [String] = { 24 | return ["/pay", 25 | "/cancel-payment", 26 | "/refund"] 27 | }() 28 | 29 | var subscribedRoutables: [ModuleRoutable.Type] = [PaymentsPresenter.self] 30 | var instantiatedRoutables: [WeakContainer] = [] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Payments/PaymentsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentsPresenter.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 28.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class PaymentsPresenter: ModuleRoutable { 12 | 13 | lazy private var wireframe = PaymentWireframe() 14 | lazy private var interactor = PaymentsInteractor() 15 | private var parameters: ModuleParameters? 16 | private var callback: ModuleCallback? 17 | 18 | static func routable() -> ModuleRoutable { 19 | return self.init() 20 | } 21 | 22 | static func getPaths() -> [String] { 23 | return ["/pay"] 24 | } 25 | 26 | func route(parameters: ModuleParameters?, path: String?, callback: ModuleCallback?) { 27 | 28 | self.parameters = parameters 29 | self.callback = callback 30 | wireframe.presentPayViewController(with: self, parameters: parameters) 31 | } 32 | 33 | func pay(amount: String?) { 34 | 35 | if let amount = amount { 36 | parameters?[PaymentsModuleParameters.suggestedAmount.rawValue] = amount 37 | interactor.pay(parameters: parameters) { [weak self] (response, error) in 38 | 39 | self?.callback?(nil, nil, response, error) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Payments/PaymentsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentsViewController.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 22/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class PaymentsViewController: StoryboardIdentifiableViewController { 13 | 14 | var presenter: PaymentsPresenter? 15 | 16 | @IBOutlet weak var amountTextField: UITextField! 17 | @IBOutlet weak var payButton: UIButton! 18 | 19 | @IBAction private func payButtonAction(_ sender: UIButton) { 20 | 21 | presenter?.pay(amount: amountTextField.text) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Modules/Payments/PaymentsWireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentsWireframe.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 29.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class PaymentWireframe: WireframeType { 13 | 14 | // We use the default storyboard, which can be changed later by injected parameter 15 | lazy var storyboard: UIStoryboard = UIStoryboard(name: "PaymentsStoryboard", bundle: nil) 16 | var presentedViewControllers = [WeakContainer]() 17 | var presentationMode: ModulePresentationMode = .none 18 | 19 | func presentPayViewController(with presenter: PaymentsPresenter, parameters: ModuleParameters?) { 20 | 21 | setPresentationMode(from: parameters) 22 | if let viewController = viewController(from: parameters) { 23 | 24 | present(viewController: viewController) 25 | guard let paymentsViewController = viewController as? PaymentsViewController else { return } 26 | paymentsViewController.presenter = presenter 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // APIClientArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 26/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum HTTPMethod: String { 12 | 13 | case GET 14 | case POST 15 | } 16 | 17 | class NetworkService { 18 | 19 | func get(scheme: String? = nil, 20 | host: String, 21 | path: String, 22 | parameters: [String: Any]?, 23 | completion: @escaping ([String: Any]?, HTTPURLResponse?, Error?) -> Void) { 24 | 25 | guard let urlRequest = request(scheme: scheme, 26 | host: host, 27 | path: path, 28 | parameters: parameters) else { 29 | return 30 | } 31 | 32 | service(urlRequest: urlRequest, completion: completion) 33 | } 34 | 35 | func post(scheme: String? = nil, 36 | host: String, 37 | path: String, 38 | parameters: [String: Any]?, 39 | completion: @escaping ([String: Any]?, HTTPURLResponse?, Error?) -> Void) { 40 | 41 | guard let urlRequest = request(scheme: scheme, 42 | host: host, 43 | path: path, 44 | parameters: parameters, 45 | httpMethod: HTTPMethod.POST.rawValue) else { 46 | return 47 | } 48 | 49 | service(urlRequest: urlRequest, completion: completion) 50 | } 51 | 52 | private func service(urlRequest: URLRequest, 53 | completion: @escaping ([String: Any]?, HTTPURLResponse?, Error?) -> Void) { 54 | 55 | let configuration = URLSessionConfiguration.`default` 56 | configuration.protocolClasses?.append(URLRouter.self) 57 | let session = URLSession(configuration: configuration) 58 | 59 | session.dataTask(with: urlRequest) { (data, urlResponse, error) in 60 | 61 | var responseBody: [String: Any]? = nil 62 | 63 | if let data = data { 64 | do { 65 | responseBody = try JSONSerialization.jsonObject(with: data) as? [String: Any] 66 | } catch { 67 | responseBody = nil 68 | } 69 | } 70 | completion(responseBody, urlResponse as? HTTPURLResponse, error) 71 | 72 | }.resume() 73 | } 74 | 75 | private func request(scheme: String? = nil, 76 | host: String, 77 | path: String, 78 | parameters: [String: Any]?, 79 | httpMethod: String? = HTTPMethod.GET.rawValue) -> URLRequest? { 80 | 81 | var components = URLComponents() 82 | components.scheme = scheme ?? "http" 83 | components.host = host 84 | components.path = path 85 | 86 | if let parameters = parameters { 87 | components.queryItems = parameters.compactMap { URLQueryItem(name: $0.key, value: String(describing: $0.value)) } 88 | } 89 | guard let requestUrl = components.url else { 90 | return nil 91 | } 92 | 93 | var urlRequest = URLRequest(url: requestUrl) 94 | urlRequest.httpMethod = httpMethod 95 | 96 | return urlRequest 97 | } 98 | } 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Routing/ApplicationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationRouter.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 14/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ModuleCallback = ([String: Any]?, Data?, URLResponse?, ResponseError?) -> Void 12 | 13 | /** 14 | Protocol defines application router, which function is 15 | 16 | - register application modules 17 | - access/open the modules 18 | - provide the callback, result of the access 19 | */ 20 | protocol ApplicationRouterType: class { 21 | 22 | var instantiatedModules: [ModuleType] { get set } 23 | var moduleQueue: DispatchQueue { get } 24 | 25 | func open(url: URL, 26 | callback: ModuleCallback?) 27 | } 28 | 29 | extension ApplicationRouterType { 30 | 31 | func open(url: URL, 32 | callback: ModuleCallback?) { 33 | 34 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), 35 | let route = components.host else { 36 | return 37 | } 38 | 39 | guard let module = instantiatedModules.filter({ $0.route == route }).first, 40 | let path = module.paths.filter({ $0 == url.path }).first else { 41 | 42 | assertionFailure("Wrong host or/and path") 43 | return 44 | } 45 | 46 | module.open(parameters: components.queryItemsDictionary, path: path) { (response, data, urlResponse, error) in 47 | 48 | callback?(response, data, urlResponse, error) 49 | } 50 | 51 | } 52 | } 53 | 54 | class ApplicationRouter: ApplicationRouterType { 55 | 56 | // TODO: This is synchronising only write access, which might be inadequate in many cases 57 | // Need to be replaced with proper full generic implementation of synchronized collection 58 | private (set) var moduleQueue = DispatchQueue(label: "com.yourbank.module.queue") 59 | 60 | // ApplicationRouter is a singleton, because it makes it easier to be accessed from anywhere to access its functions/services 61 | static let shared = ApplicationRouter() 62 | 63 | // We have registered 2 modules for now... 64 | var instantiatedModules: [ModuleType] = [PaymentModule(), 65 | LoginModule()] 66 | } 67 | 68 | @objc class URLRouter: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate { 69 | 70 | // TODO: This is synchronisyng only write access, which might be inadequate in many cases 71 | // Need to be replaced with proper full generic implementation of synchronized collection 72 | private (set) var moduleQueue = DispatchQueue(label: "com.yourbank.module.queue") 73 | 74 | // MARK: URLProtocol methods overriding 75 | 76 | override class func canInit(with task: URLSessionTask) -> Bool { 77 | 78 | // Check if there's internal app schema that matches the one in the URL 79 | guard let url = task.originalRequest?.url, 80 | url.containsInAppSchema() else { 81 | return false 82 | } 83 | 84 | // Check if there's a path in the module that matches the one in the URL 85 | guard let module = ApplicationRouter.shared.instantiatedModules.filter({ $0.route == task.originalRequest?.url?.host }).first, 86 | let _ = module.paths.filter({ $0 == task.originalRequest?.url?.path }).first else { 87 | return false 88 | } 89 | return true 90 | } 91 | 92 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 93 | return request 94 | } 95 | 96 | 97 | override func startLoading() { 98 | 99 | guard let url = request.url else { 100 | return 101 | } 102 | 103 | ApplicationRouter.shared.open(url: url) { (response, data, urlResponse, error) in 104 | 105 | // TODO: Calling URLSessionDataDelegate methods to return the response 106 | } 107 | } 108 | 109 | override func stopLoading() { 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Routing/ApplicationServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationServices.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 28.06.18. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class ApplicationServices { 13 | 14 | // ApplicationServices is a singleton, because it makes it easier to be accessed from anywhere to access its functions/services 15 | static let shared = ApplicationServices() 16 | let appRouter = ApplicationRouter() 17 | 18 | func pay(amount: Double, 19 | paymentToken: String?, 20 | completion: @escaping (() -> Void)) { 21 | 22 | // MARK Urls to make the logic more readable, not to clog the logic in closures in the service 23 | func payUrl(amount: Double, paymentToken: String?) -> URL? { 24 | 25 | guard let moduleUrl = URL(schema: "yourbank", 26 | host: "payments", 27 | path: "/pay", 28 | parameters: ["amount": String(amount), 29 | "token": paymentToken ?? "", 30 | "presentationMode": "navigationStack", 31 | "viewController": "PaymentsViewControllerId"]) else { return nil } 32 | return moduleUrl 33 | } 34 | 35 | func loginUrl() -> URL? { 36 | 37 | // We add view controller, because there could be business cases where we could already pass 38 | // username and password and no view controller for their input would be needed 39 | guard let moduleUrl = URL(schema: "yourbank", 40 | host: "login", 41 | path: "/payment-token", 42 | parameters: ["viewController": "LoginViewControllerId", 43 | "presentationMode": "modal"]) else { return nil } 44 | return moduleUrl 45 | } 46 | 47 | // MARK Logic - Start 48 | // Summary: If PaymentsModule fails because of missing or expired or faulty `paymentToken`, 49 | // then LoginModule is called and `paymentToken` by using its login window and user putting 50 | // username and password in. 51 | // All logic and orchestration between these 2 modules is contained in the code below and both 52 | // modules do not know anything about each other 53 | guard let paymentUrl = payUrl(amount: amount, paymentToken: paymentToken), 54 | let loginUrl = loginUrl() else { return } 55 | 56 | appRouter.open(url: paymentUrl) { [weak self] (response, responseData, urlResponse, error) in 57 | 58 | if case .unauthorized401(_)? = error { 59 | 60 | self?.appRouter.open(url: loginUrl) { (response, responseData, urlResponse, error) in 61 | 62 | if let response = response, 63 | let paymentToken = response["paymentToken"] as? String { 64 | 65 | self?.pay(amount: amount, paymentToken: paymentToken, completion: { 66 | completion() 67 | }) 68 | } else { 69 | completion() 70 | } 71 | } 72 | } 73 | else { 74 | completion() 75 | } 76 | 77 | } 78 | // MARK Logic - End 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Routing/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Module.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 14/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public typealias ModuleParameters = [String: String] 13 | 14 | public struct ModuleConstants { 15 | 16 | struct UrlParameter { 17 | 18 | static let viewController = "viewController" 19 | static let presentationMode = "presentationMode" 20 | static let storyboard = "storyboard" 21 | } 22 | } 23 | 24 | public enum ResponseError: Error { 25 | 26 | case serializationFailed 27 | case taskCancelled 28 | case badRequest400(error: Error?) 29 | case unauthorized401(error: Error?) 30 | case forbidden403(error: Error?) 31 | case notFound404(error: Error?) 32 | case other400(error: Error?) 33 | case serverError500(error: Error?) 34 | case other 35 | 36 | init?(error: Error?, response: HTTPURLResponse?) { 37 | 38 | let responseCode: Int 39 | if let response = response { 40 | responseCode = response.statusCode 41 | } else { 42 | responseCode = 0 43 | self = .other 44 | } 45 | 46 | switch responseCode { 47 | case 200..<300: 48 | return nil 49 | case 400: 50 | self = .badRequest400(error: error) 51 | case 401: 52 | self = .unauthorized401(error: error) 53 | case 403: 54 | self = .forbidden403(error: error) 55 | case 404: 56 | self = .notFound404(error: error) 57 | case 405..<500: 58 | self = .other400(error: error) 59 | case 500..<600: 60 | self = .serverError500(error: error) 61 | default: 62 | self = .other 63 | } 64 | } 65 | } 66 | 67 | /** 68 | Application module represents a group off all the classes that implement a certain functionality of the module, like: 69 | 70 | - Storyboard 71 | - View Controllers 72 | - Views, specific to the module 73 | - Presenters, View Models and other Client architecture classes 74 | - ... 75 | 76 | Every module needs to identify itself with unique application route/domain which is queried by `ModuleHub` 77 | */ 78 | public protocol ModuleType: class { 79 | 80 | /** 81 | Every module needs to identify itself with a certain route/domain 82 | 83 | - returns: 84 | String that represents the route, domain, like _"/module-name"_ 85 | */ 86 | var route: String { get } 87 | 88 | /** 89 | Paths, which represent methods/functionalities a module has, module capabilities, actually 90 | 91 | - returns: 92 | Array of path strings 93 | */ 94 | var paths: [String] { get } 95 | 96 | /** 97 | This array contains all the potential types module can let to route to. 98 | Reflection is used for memory savvy approach 99 | */ 100 | var subscribedRoutables: [ModuleRoutable.Type] { get set } 101 | var instantiatedRoutables: [WeakContainer] { get set } 102 | 103 | /** 104 | Function has to implement start of the module 105 | 106 | - parameters: 107 | - parameters: Simple dictionary of parameters 108 | - path: Path which is later recognised by specific module and converted to possible method 109 | */ 110 | func open(parameters: ModuleParameters?, 111 | path: String?, 112 | callback: ModuleCallback?) 113 | 114 | /** 115 | This function could be called from `open` or any other for that matter. 116 | It facilitates any potentially additionally setup, that a module would eventually need. 117 | */ 118 | func setup(parameters: ModuleParameters?) 119 | } 120 | 121 | public extension ModuleType { 122 | 123 | func open(parameters: ModuleParameters?, path: String?, callback: ModuleCallback?) { 124 | 125 | let subscribedRoutableTypes = subscribedRoutables.filter { subscribedType in 126 | 127 | let matchedType = subscribedType.getPaths().filter { $0 == path } 128 | if matchedType.isEmpty == false { 129 | return true 130 | } 131 | else { 132 | return false 133 | } 134 | } 135 | 136 | guard let subscribedRoutableType = subscribedRoutableTypes.first else { return } 137 | let routables: [WeakContainer] = instantiatedRoutables.filter { routable in 138 | 139 | return subscribedRoutableType == type(of: routable.value!) 140 | } 141 | if let routable = routables.first?.value { 142 | routable.route(parameters: parameters, 143 | path: path, 144 | callback: callback) 145 | } 146 | else { 147 | let routable = subscribedRoutableType.routable() 148 | instantiatedRoutables.append(WeakContainer(value: routable)) 149 | routable.route(parameters: parameters, 150 | path: path, 151 | callback: callback) 152 | } 153 | 154 | setup(parameters: parameters) 155 | } 156 | 157 | func setup(parameters: ModuleParameters?) { } 158 | } 159 | 160 | /** 161 | Protocol should be adopted by the classes, which are routed directly by a `Module` and 162 | be registered in it. 163 | */ 164 | public protocol ModuleRoutable { 165 | 166 | /** 167 | Every class which wants to be routed by `Module` needs to identify itself with a certain path/method 168 | 169 | - returns: 170 | Collection of String that represents paths 171 | */ 172 | static func getPaths() -> [String] 173 | 174 | /** 175 | Function which is a workaround for the weak reflection possibilites to 176 | create an instance of `ModuleRoutable` from `Class.Type` 177 | */ 178 | static func routable() -> ModuleRoutable 179 | 180 | func route(parameters: ModuleParameters?, 181 | path: String?, 182 | callback: ModuleCallback?) 183 | } 184 | 185 | public class StoryboardIdentifiableViewController: UIViewController { 186 | 187 | var storyboardId: String? = nil 188 | } 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Routing/WireframeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardModule.swift 3 | // ModuleArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 19/05/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public enum ModulePresentationMode: String { 13 | 14 | case none 15 | case root 16 | case navigationStack 17 | case modal 18 | } 19 | 20 | /** 21 | This protocol will contain functionality that is used primarily by modules that origin from Storyboard 22 | */ 23 | public protocol WireframeType: class { 24 | 25 | var storyboard: UIStoryboard { get set } 26 | var presentationMode: ModulePresentationMode { get set } 27 | var presentedViewControllers: [WeakContainer] { get set } 28 | 29 | /** 30 | Returns `initialViewController`, if its name is specified in parameters, where key by convention is equal to `viewController` 31 | 32 | - parameters: Dictionary that contains key-value pairs of different parameters from URL. 33 | If it contains the key `viewController` then its value is used as name of view controller 34 | - returns: UIViewController, which is default from storyboard of the one that was specified by parameters 35 | */ 36 | func initialViewController(from parameters:[String: String]?) -> UIViewController? 37 | 38 | /** 39 | Sets `presentationMode`, if its name is specified in parameters, where key by convention is equal to `presentationMode` 40 | 41 | - parameters: Dictionary that contains key-value pairs of different parameters from URL. 42 | If it contains the key `presentationMode` then its value is used to init view controller 43 | If it's `nil`, then `.root` is assumed. 44 | */ 45 | func setPresentationMode(from parameters:[String: String]?) 46 | 47 | /** 48 | Presents view controller according to `presentationMode` 49 | 50 | - parameters: 51 | - viewController: UIViewController to be presented 52 | - navigationViewController: if `presentationMode` is equal to `navigationStack`, then this value will be used to get 53 | */ 54 | func present(viewController: UIViewController) 55 | 56 | /** 57 | Function with generic set of parameters. Should be called from `StoryboardModuleType`. 58 | */ 59 | func setupWireframe(parameters: ModuleParameters?) 60 | } 61 | 62 | 63 | extension WireframeType { 64 | 65 | func setupWireframe(parameters: ModuleParameters?) { 66 | 67 | // Guard prevents from initial VC being added again each time `open` function is 68 | // run from the Module 69 | // Each time it's called, `presentedViewControllers` is cleared from empty containers 70 | presentedViewControllers = presentedViewControllers.filter { $0.value != nil } 71 | guard presentedViewControllers.isEmpty else { return } 72 | 73 | if let storyboardName = parameters?[ModuleConstants.UrlParameter.storyboard] { 74 | storyboard = UIStoryboard(name: storyboardName, bundle: nil) 75 | } 76 | 77 | setPresentationMode(from: parameters) 78 | if let viewController = initialViewController(from: parameters) { 79 | present(viewController: viewController) 80 | presentedViewControllers.append(WeakContainer(value: viewController)) 81 | } 82 | } 83 | 84 | func viewController(from parameters:[String: String]?) -> UIViewController? { 85 | 86 | guard let viewControllerName = parameters?[ModuleConstants.UrlParameter.viewController] else { 87 | return nil 88 | } 89 | let viewController = storyboard.instantiateViewController(withIdentifier: viewControllerName) 90 | 91 | // If VC doesn't inherit from to `StoryboardIdentifiable`, then we assume caller will 92 | // use it on his own discretion, so we return it. 93 | guard let identifiableVc = viewController as? StoryboardIdentifiableViewController else { 94 | return viewController 95 | } 96 | 97 | // One particular view controller should be presented only once if 98 | // it conforms to the StoryboardIdentifiable protocol 99 | // Here we add it to the array of already presented 100 | let identifier = parameters?[ModuleConstants.UrlParameter.viewController] 101 | 102 | // Each time it's called, `presentedViewControllers` is cleared from empty containers 103 | presentedViewControllers = presentedViewControllers.filter { $0.value != nil } 104 | let instantiatedVc = presentedViewControllers.filter { wrappedVc in 105 | 106 | let storyboardId = wrappedVc.value as? StoryboardIdentifiableViewController 107 | return storyboardId?.storyboardId == identifier ? true : false 108 | } 109 | 110 | if instantiatedVc.isEmpty == true { 111 | 112 | identifiableVc.storyboardId = identifier 113 | presentedViewControllers.append(WeakContainer(value: identifiableVc)) 114 | return identifiableVc 115 | } 116 | else { 117 | return nil 118 | } 119 | } 120 | 121 | /** 122 | This function could be private, too, but we assume module might want to inject some other 123 | properties to it, therefore we hand over control to the module, after initial VC is instantiated 124 | */ 125 | func initialViewController(from parameters:[String: String]?) -> UIViewController? { 126 | 127 | guard let viewControllerName = parameters?[ModuleConstants.UrlParameter.viewController] else { 128 | 129 | return storyboard.instantiateInitialViewController() 130 | } 131 | return storyboard.instantiateViewController(withIdentifier: viewControllerName) 132 | } 133 | 134 | 135 | func setPresentationMode(from parameters: [String: String]?) { 136 | 137 | guard let mode = parameters?[ModuleConstants.UrlParameter.presentationMode], 138 | let modulePresentationMode = ModulePresentationMode(rawValue: mode) else { 139 | 140 | presentationMode = .root 141 | return 142 | } 143 | presentationMode = modulePresentationMode 144 | } 145 | 146 | 147 | func present(viewController: UIViewController) { 148 | 149 | DispatchQueue.main.async { 150 | 151 | func presentableVc() -> UIViewController? { 152 | 153 | guard let delegate = UIApplication.shared.delegate as? AppDelegate, 154 | let window = delegate.window, 155 | let rootViewController = window.rootViewController else { return nil } 156 | 157 | return rootViewController.topmostNavigationController()?.childViewControllers.last?.topPresentedController() ?? rootViewController.topPresentedController() 158 | } 159 | 160 | switch self.presentationMode { 161 | 162 | case .navigationStack: 163 | 164 | guard let navController = UIViewController.topPresentedController()?.topmostNavigationController() else { 165 | 166 | assertionFailure("ModuleHub: attempt to push controller on the top navigation controller failed - no UINavigationController found") 167 | return 168 | } 169 | 170 | navController.pushViewController(viewController, animated: true) 171 | 172 | case .modal: 173 | 174 | // If we want to use modal with navigation bar, we can simply set it up in storyboard. 175 | // We could do this here as well, if we'd have some other app global navigation scenarios. 176 | 177 | presentableVc()?.present(viewController, animated: true, completion: nil) 178 | 179 | case .none: () 180 | 181 | case .root: 182 | 183 | // Default is .root 184 | fallthrough 185 | 186 | default: 187 | 188 | let delegate = UIApplication.shared.delegate as? AppDelegate 189 | delegate?.window?.rootViewController = viewController 190 | delegate?.window?.makeKeyAndVisible() 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Storyboards/LoginStoryboard.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/Storyboards/PaymentsStoryboard.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 | 27 | 28 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ModuleArchitectureDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // APIClientArchitectureDemo 4 | // 5 | // Created by Mladen Despotovic on 20/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var interactor: PaymentsInteractor? 14 | 15 | override func viewDidLoad() { 16 | 17 | super.viewDidLoad() 18 | title = "Main Screen" 19 | } 20 | 21 | @IBAction func toPayments() { 22 | 23 | ApplicationServices.shared.pay(amount: 123.00, paymentToken: nil, completion: {}) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /ModuleArchitectureDemoTests/APIClientArchitectureDemoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientArchitectureDemoTests.swift 3 | // APIClientArchitectureDemoTests 4 | // 5 | // Created by Mladen Despotovic on 20/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import APIClientArchitectureDemo 11 | 12 | class APIClientArchitectureDemoTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ModuleArchitectureDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ModuleArchitectureDemoUITests/APIClientArchitectureDemoUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientArchitectureDemoUITests.swift 3 | // APIClientArchitectureDemoUITests 4 | // 5 | // Created by Mladen Despotovic on 20/03/2018. 6 | // Copyright © 2018 Mladen Despotovic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class APIClientArchitectureDemoUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // 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. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ModuleArchitectureDemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # module-architecture-demo -------------------------------------------------------------------------------- /Text.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf400 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw11900\paperh16840\margl1440\margr1440\vieww19160\viewh13220\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 SOLID?\ 9 | \ 10 | Well, yes. Regardless of development paradigms and forms, we adhere to and comply with some basic principles of OOP, which we try to follow as much as possible to create the code that will get as close as possible to the technical and organisational goals. Which are such goals? I would think of at least of the ones below:\ 11 | \ 12 | - easy to read\ 13 | - easy to understand\ 14 | - easy to maintain\ 15 | - easy to debug\ 16 | - easy to test\ 17 | - easy to change\ 18 | - easy to swap parts\ 19 | - easy to add different scenarios\ 20 | - easy to extend existing functionality\ 21 | - easy to add new functionality\ 22 | \ 23 | Gosh, everything needs to be easy nowadays.. Are we lazy or something? No, it\'92s all about making our business to run efficiently during the code lifecycle and lifetime, actually.\ 24 | \ 25 | Single Responsibility principle is probably one of the most used and abused. It\'92s most important principle to reach most of the goals above to the certain degree. However, on its own, it has limitations which prevent us to progress from better code to great.\ 26 | \ 27 | Its opposite is monolithic code, which old definitions argue to be more efficient, than fragmented, less coupled, but that would be the case only if we observe speed of execution, which is becoming the least important aspect of app development and delivery, so I won\'92t spend any time to reason anything, I think the picture is generally clear to all of us. \ 28 | \ 29 | \ 30 | From Coupled to Loosely Coupled and finally to Decoupled\ 31 | \ 32 | Following Single Responsibility principle will fragment our code (in a positive way) into separate, let\'92s say, modules, classes, functions even, which would definitively make our code: \ 33 | \ 34 | - easy to read\ 35 | - easy to understand,\ 36 | - easy to maintain,\ 37 | - easy to debug,\ 38 | \ 39 | but sometimes not so:\ 40 | \ 41 | - easy to test\ 42 | - easy to change,\ 43 | \ 44 | and definitively not at all easy to:\ 45 | \ 46 | - swap parts\ 47 | - add different scenarios\ 48 | - extend existing functionality\ 49 | - add new functionality\ 50 | \ 51 | \ 52 | Coupled: Functionality is distributed through several classes, but types are explicitly referenced:\ 53 | \ 54 | \ 55 | Definitively not so easy to test, because it may prove that some types will not be easy to create mocks for. Still, not critical and still manageable in terms of testing.\ 56 | Changes are still manageable as long as they are contained in a class. As soon as we want to introduce changes to the whole types, we can get into the trouble, because we shall have to change the code in other classes which references them.\ 57 | If we would want to add different scenarios, we would have to resort to if-else and switch statements, type checking, flag checking, all the nasty stuff\'85\ 58 | \ 59 | \ 60 | Loosely coupled: Classes are connected through interfaces/protocols\ 61 | \ 62 | This more or less solves all the issues above. We can definitely mock with ease, we can inject actual implementation to a calling object through the interface, thus implementing different scenarios with strategy pattern, apply abstract factories, \'85 all the goodies of OOP.\ 63 | \ 64 | We solve all the issues \'85. within the same platform/app/system.\ 65 | \ 66 | \ 67 | Decoupled: Systems connect through protocols, ruled by conventions, generic and app specific.\ 68 | \ 69 | Nice examples of the above are for example HTTP, REST, Microservices. The difference is, we don\'92t talk about the single system anymore, we talk about distributed systems, heterogeneous in terms of technologies used, actually, technologies don\'92t matter anymore, it\'92s only about the messages and convention which defines their payload, it's a vocabulary, dictionary, which is recognised by both.\ 70 | \ 71 | Fine. About what does this have to do with the apps and client architecture? Well, it has. Most of you have already implemented deep links in your app and you could see, how a URL request, formed upon convention was deconstructed and used to activate and call exactly the functionality/classes, that it needed, thus basically establish unilateral connection between your email or web page and your classes in your app, which effectively become URL resource. Magical isn\'92t it?! :)\ 72 | \ 73 | \ 74 | URL\ 75 | \ 76 | The name says it all: it\'92s universal address of the resource. Add HTTP/REST and you have a convention to asynchronously pass requests and responses between resources. \ 77 | \ 78 | \ 79 | Separation of concerns in your app\ 80 | \ 81 | I guess there are many ways on a different level, but I guess it makes sense to start separating the concerns on the feature streams, feature clusters, let us call them feature modules, or short: modules.\ 82 | You can easily imagine a simple banking app, which would have modules like:\ 83 | \ 84 | - login\ 85 | - transaction list\ 86 | - payments\ 87 | - \'85\ 88 | \ 89 | Now, imagine:\ 90 | - you\'92d want to access any of these features/modules independently through deep links.\ 91 | - some of these modules/services might have dependencies between each other\ 92 | - that you might use different modules for the same purpose ( using password-based login module or TouchID/FaceID/PIN-based, for example)\ 93 | - you want to totally encapsulate all functionality in every module, thus making app completely modular and turn modules into the building blocks with its own communication convention/protocol.\ 94 | \ 95 | Now, that\'92s a loooong wish list!\ 96 | \ 97 | \ 98 | Application Router to the rescue\ 99 | \ 100 | This is probably the point, where we shall start to materialise all our previous findings and wishes. \ 101 | What is Application Router? It\'92s a class. It\'92s a singleton even!!! \ 102 | It\'92s very short and lean. It\'92s storage are \'91registered\'92 classes, which means the classes it knows about and are compliant to certain protocols which makes them at least to identify themselves. All the Application Router does is:\ 103 | \ 104 | - receiving valid URL request\ 105 | - finding Module, which will respond to the host from the URL\ 106 | - call this module with the path, which represents the function/method and potential parameters\ 107 | - return callback with URLResponse, eventual response dictionary and eventual Error\ 108 | \ 109 | Sounds almost like an RESTful API router, doesn\'92t it? Well, it almost is. But it\'92s even more generic, it\'92s more like a URL router.\ 110 | \ 111 | Code is pretty much simple: we have a property as collection of registered ModuleTypes and function to open module, both defined in ApplicationRouterType.\ 112 | \ 113 | \ 114 | How about the Module? What is Module, anyway?\ 115 | \ 116 | Module is a simple class, which represents a gateway between a group of types that perform a set of tasks that define a common global functionality on an app level, we could call it: a service and the rest of the app (like other modules) and outside world (like deep links) and even other technologies bundled in the app (like React Native).\ 117 | \ 118 | Sounds pretty much attractive. Sounds almost like a Swiss knife\'85 \ 119 | Protocol ModuleType pretty much defines everything what needs to work in it: \ 120 | \ 121 | - route String, which uniquely identifies the module and map to the host from the URL\ 122 | - collection of paths, which uniquely identify all the functions of the module and map to the path from the URL\ 123 | - open function which is a simple gateway to the module, providing route, path, parameters and give back the response in form of standard URL session.\ 124 | \ 125 | \ 126 | Module Router Class\ 127 | \ 128 | We want to keep our Module class as lean as possible. It should be merely a gateway to what the module can offer.\ 129 | As with Application, we need Router for a module, too. ModuleRouter then becomes a standard member of module architecture and all it does is calling other objects within the module architecture, based on path from the URL.\ 130 | \ 131 | \ 132 | \'85 one more thing?\ 133 | \ 134 | Yes. Well, does our app contain only modules? It could mostly contain only modules calling each other. But if we go back a bit when we described their role, modules seems more like services, which provide certain functionality. They could be dependent on each other, but there also might be need to orchestrate them and even queue them sometimes.\ 135 | \ 136 | That\'92s why we introduced a class called ApplicationServices, which exposes full services to any part of our app. In our case, we simply call pay service from Application Delegate, but you can get an idea, how this might work in many other scenarios, of course.\ 137 | \ 138 | \'85 one last thing?\ 139 | \ 140 | There\'92s something I\'92d like to mention. So, imagine you have the code that for whatever reason, you don\'92t want to wrap into modules, thus making it easy for further integration. It could be even your Javascript code in React Native part of your app, if you have hybrid app, for example. \ 141 | I still think that it\'92s very easy to wrap something that implements 2 things, but in the case, we don\'92t want to, there\'92s simple solution, demonstrated in NonCompliantModule.\ 142 | \ 143 | Instead of using Application Router, we have simply called an API through URLSession in out NetworkService. \ 144 | Beside that, we have created URLRouter class, which implements URLProtocol. You might remember this class, which is used for creating REST stubs in most of the mocking libraries.\ 145 | In our case, this class will read the schema from the URLRequest and if the latter contains an internal schema, like \'91tandem\'92 in our case, which we have set in Info.plist, then this class will intercept this call within canInit(with function and call Application Router. \ 146 | \ 147 | In case of NonCompliantModule, we can see that it calls \'91/login\'92 method in \'91login\'92 module same as it would call any other REST resource with the only difference that it calls it through \'91tandem rather than \'91http\'92 schema.\ 148 | \ 149 | \ 150 | Next step will be showing, how can we work within the modules.\ 151 | \ 152 | \ 153 | \ 154 | Non-Conforming Modules\ 155 | \ 156 | \ 157 | It may happen, that we will have to include in our project the code, which doesn\'92t comply with the architecture we are presenting in this series. Reasons might be various: we might have legacy code, badly separated by concerns, perhaps we have React Native bridging with a lot of already written Javascript code, for which we have no possibility to change interfacing to the Native side, etc.\ 158 | \ 159 | This chapter will show, how we shall use URLProtocol and iOS\'92s simple URL Loading system to turn our modules into classic HTTP API thus making our modules open and compatible with any code in the same way as we would do it for deep links outside the app.\ 160 | \ 161 | A simple Network Service\ 162 | \ 163 | We have created a simple NetworkService class, which provides us with two exposed functions get and post. Besides that, we have two private functions. First, the request is pretty much obvious. The only thing that seems a bit fishy is the scheme parameter. It\'92s handled pretty much simply: if no scheme is passed as a parameter, then it\'92s set the default to \'91http\'92. We are happy with this for now.\ 164 | \ 165 | Then we have the service function since we are reusing this code it in both HTTP methods, get and post. But there is one line of code, which makes all this very interesting: configuration.protocolClasses?.append(URLRouter.self). This method of URLSessionConfiguration gives the possibility to register our own class URLRouter which implements URLProtocol. Let\'92 see why is this needed.\ 166 | \ 167 | \ 168 | URLRouter\ 169 | \ 170 | This is our implementation of 2 protocols: URLSessionDataDelegate, URLSessionTaskDelegate and above all, subclassing the abstract class URLProtocol (which doesn\'92t have the best name, perhaps). For all of you, who have ever used old mocking framework for testing URL connections, URLProtocol was usually used for it. Many of us used it directly ourselves, to create our own network stubbing infrastructure. I won\'92t go too much into details, because there\'92s quite a lot written about it already, but let me just explain the role of this protocol in our example. \ 171 | So, what we do with the URLRouter is very simple:\ 172 | \ 173 | * check in override class func canInit(with task: URLSessionTask) -> Bool if URLRequest from the task has the schema, which matches our internal schema, which we defined in previous chapter\ 174 | * then we check if URL contains host and path that have a match in our ApplicationRouter\ 175 | * if the check is positive, then URLLoading system calls our override func startLoading() function, where we simply call an appropriate module through are ApplicationRouter and job done!\ 176 | \ 177 | Well, not completely, so, we haven\'92t created the returning response boilerplate here, but that is not the most important right now. So, what is the next step? Well, the \'91naughty\'92 module, I guess!\ 178 | \ 179 | \ 180 | Non-conforming Module\ 181 | \ 182 | We have created a very simple class, called NonConformingModule, which doesn\'92t implement any of the architecture, we were talking about in this series from the first chapter onwards. The only thing it does is the login to get bearerToken and this is where the login function in LoginModuleRouter is used, which we mentioned before. The only thing we need to do is to instantiate this module and call login function and the following will happen:\ 183 | \ 184 | * NonConformingModule calls directly Networking service as a normal HTTP request, but it passes \'91tandem\'92 as schema and parameters for /login method of login module\ 185 | * NetworkService calls to iOS\'92s URL loading system in the same way as it would for any other HTTP request.\ 186 | * Registered URLRouter class intercepts the URLSessionTask, sees scheme from URL matches an internal one and confirms interception and creates the instance of itself\ 187 | * URLRouter calls ApplicationRouter with URL, which opens LoginModule the latter calls LogineModuleRouter and the rest you already know\'85\ 188 | \ 189 | This is a showcase, how the URL as a universal access object can be used directly through URL Loading system to access our Module Architecture infrastructure. \ 190 | \ 191 | \ 192 | \ 193 | StoryboardModule\ 194 | \ 195 | \ 196 | View controllers and navigation controllers can have a lot of different ways how to get initialised, setup, styled and customised\ 197 | \ 198 | Injecting the storyboard:\ 199 | \ 200 | 1. Storyboard can be part of the module itself\ 201 | \ 202 | True. Also explicitly referenced. For modules that will change very little if at all, used in different apps, this might make sense. It might offer some different fixed styling options, which could be exposed as parameter in module contract, in terms of fonts and colour schema, but this might become too limited too soon.\ 203 | \ 204 | 2. Storyboard is part of the app\ 205 | \ 206 | Storyboard is still referred with the fixed ID from the code in the module, but the storyboard is provided from the main app and developer has all the freedom of using different styles and even View Controller classes in some places, if necessary.\ 207 | \ 208 | 3. Storyboard ID is passed to module as a parameter.\ 209 | \ 210 | This option is most flexible. We can create not only our own Storyboard, but also more of them and inject them to the module when we call it by passing the storyboard ID. We have complete freedom of what to do with styling, injecting our own styling classes, using our own subclasses of UINavigationController and UIViewController. This means hat our module can actually have its default Storyboard, but we can inject our own if we like. It\'92s even more than than that: we could inject different storyboards from different user workflows!\ 211 | That will also be the our choice in the code example which will continue our existing project. \ 212 | \ 213 | \ 214 | Injecting the Initial View Controller\ 215 | \ 216 | This option can be pretty much flexible as well. \ 217 | \ 218 | 1. Initial View Controller ID is not passed as a parameter.\ 219 | \ 220 | Storyboard uses simply the default initial view controller from its .nib file\ 221 | \ 222 | 2. We pass the initial view controller ID as a parameter\ 223 | \ 224 | Storyboard initialised the View controlled identified by a passed parameter. If it\'92s not found, it can simply default to the one from its .nib file.\ 225 | \ 226 | View Controller in the storyboard can of course already have a lot of customisations made, like styling, it can also inject objects through .nib file, especially styling objects, for example.\ 227 | \ 228 | \ 229 | \ 230 | Presentation mode\ 231 | \ 232 | We understand now, how and where from shall we instantiate initial view controller, but that\'92s not the end of the story. We have really many, many ways, how to present a view controller. Let\'92s just see a few:\ 233 | \ 234 | - pure full screen modal\ 235 | - modal wrapped in Navigation Controller\ 236 | - modal of customised dimensions\ 237 | - pushed on the current navigation stack\ 238 | - instantiated in tab bar controller\ 239 | - \'85\ 240 | \ 241 | We could, of course, go deeper and talk about many derivatives of the above, but let\'92s settle with what we have found out already.\ 242 | \ 243 | The question which arises to me first here is how to keep decoupled architecture and still create and present module\'92s own view controllers with relation to the ones that the module should obviously not be aware about or reference them through specific type (base condition to keep our module decoupled and highly reusable). We also need to understand that initial view controller from one specific module is part of module\'92s own storyboard and definitively has no idea about who should present it. Moreover, this can differ regarding to the module/service which opened the module anyway. Same module can be opened from different parts of app and therefore its view controller can be presented/pushed each time by another different one\'85\ 244 | \ 245 | As much as this sounds complex or even impossible it\'92s kind of relatively easy to implement it. This is the list of potential solutions:\ 246 | \ 247 | - modal view controllers can be simply presented either from rootViewController, topViewController from the topmost UINavigationController in the stack from rootViewController onwards or basically from anywhere else\ 248 | - view controller can be simply pushed either to the topmost UINavigationController in the stack from rootViewController or we could simply push new view controller to the app specific UINavigationController which is not a problem, because StoryboardModuleType will have this implementation in its extension thus independent of the module.\ 249 | - it can be also instantiated within tab view controller, which is same as the above app specific, thus the StoryboardModuleType extension can implement it referring to the app specyfic type, leaving modules decoupled from it again\ 250 | \ 251 | Our example will use modal presentation to implement LoginModule.\ 252 | \ 253 | \ 254 | XXX\ 255 | \ 256 | \ 257 | \ 258 | \ 259 | \ 260 | \ 261 | \ 262 | \ 263 | \ 264 | \ 265 | \ 266 | \ 267 | \ 268 | \ 269 | \ 270 | \ 271 | \ 272 | \ 273 | \ 274 | \ 275 | \ 276 | \ 277 | \ 278 | \ 279 | } --------------------------------------------------------------------------------