├── LICENSE ├── NavigationRouter.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── tiagohenriques.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── tiagohenriques.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── NavigationRouter ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Coordinator │ ├── Coordinator.swift │ ├── CoordinatorStack.swift │ ├── Routable.swift │ └── Routes.swift ├── Demo │ └── router_navigation_demo.gif ├── Extensions │ └── DateExtensions.swift ├── Managers │ └── FavouritesManager.swift ├── Models │ ├── Article.swift │ └── Issue.swift ├── NavigationRouterApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Views │ ├── AppView.swift │ ├── ArticlesView.swift │ ├── FavouritesView.swift │ └── IssueView.swift ├── NavigationRouterTests └── AppRoutesCoordinatorTests.swift ├── NavigationRouterUITests ├── NavigationRouterUITests.swift └── NavigationRouterUITestsLaunchTests.swift └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tiago Henriques 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 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A8510BC92CDF6B9700EC1BA4 /* router_navigation_demo.gif in Resources */ = {isa = PBXBuildFile; fileRef = A8510BC82CDF6B9700EC1BA4 /* router_navigation_demo.gif */; }; 11 | A898C3492CE260C1006ED5B5 /* Routable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A898C3482CE260C1006ED5B5 /* Routable.swift */; }; 12 | A898C34B2CE26147006ED5B5 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A898C34A2CE26147006ED5B5 /* Coordinator.swift */; }; 13 | A898C34D2CE26336006ED5B5 /* CoordinatorStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A898C34C2CE26336006ED5B5 /* CoordinatorStack.swift */; }; 14 | A898C3502CE264A5006ED5B5 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A898C34F2CE264A5006ED5B5 /* Routes.swift */; }; 15 | A8FCF2562CDD5A6F008E8EF6 /* NavigationRouterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2552CDD5A6F008E8EF6 /* NavigationRouterApp.swift */; }; 16 | A8FCF25A2CDD5A70008E8EF6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A8FCF2592CDD5A70008E8EF6 /* Assets.xcassets */; }; 17 | A8FCF25D2CDD5A70008E8EF6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A8FCF25C2CDD5A70008E8EF6 /* Preview Assets.xcassets */; }; 18 | A8FCF2672CDD5A70008E8EF6 /* AppRoutesCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2662CDD5A70008E8EF6 /* AppRoutesCoordinatorTests.swift */; }; 19 | A8FCF2712CDD5A70008E8EF6 /* NavigationRouterUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2702CDD5A70008E8EF6 /* NavigationRouterUITests.swift */; }; 20 | A8FCF2732CDD5A70008E8EF6 /* NavigationRouterUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2722CDD5A70008E8EF6 /* NavigationRouterUITestsLaunchTests.swift */; }; 21 | A8FCF2812CDD5A9B008E8EF6 /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2802CDD5A9B008E8EF6 /* Article.swift */; }; 22 | A8FCF2842CDD5AB8008E8EF6 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2832CDD5AB8008E8EF6 /* DateExtensions.swift */; }; 23 | A8FCF28C2CDD5B00008E8EF6 /* IssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF28B2CDD5B00008E8EF6 /* IssueView.swift */; }; 24 | A8FCF28E2CDD5B20008E8EF6 /* FavouritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF28D2CDD5B20008E8EF6 /* FavouritesView.swift */; }; 25 | A8FCF2902CDD5B31008E8EF6 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF28F2CDD5B31008E8EF6 /* AppView.swift */; }; 26 | A8FCF2922CDD5D90008E8EF6 /* ArticlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2912CDD5D90008E8EF6 /* ArticlesView.swift */; }; 27 | A8FCF2942CDD5F6F008E8EF6 /* Issue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2932CDD5F6F008E8EF6 /* Issue.swift */; }; 28 | A8FCF2992CDD6139008E8EF6 /* FavouritesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF2982CDD6139008E8EF6 /* FavouritesManager.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | A8FCF2632CDD5A70008E8EF6 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = A8FCF24A2CDD5A6F008E8EF6 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = A8FCF2512CDD5A6F008E8EF6; 37 | remoteInfo = NavigationRouter; 38 | }; 39 | A8FCF26D2CDD5A70008E8EF6 /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = A8FCF24A2CDD5A6F008E8EF6 /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = A8FCF2512CDD5A6F008E8EF6; 44 | remoteInfo = NavigationRouter; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | A8510BC82CDF6B9700EC1BA4 /* router_navigation_demo.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = router_navigation_demo.gif; sourceTree = ""; }; 50 | A898C3482CE260C1006ED5B5 /* Routable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routable.swift; sourceTree = ""; }; 51 | A898C34A2CE26147006ED5B5 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 52 | A898C34C2CE26336006ED5B5 /* CoordinatorStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorStack.swift; sourceTree = ""; }; 53 | A898C34F2CE264A5006ED5B5 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; 54 | A8FCF2522CDD5A6F008E8EF6 /* NavigationRouter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NavigationRouter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | A8FCF2552CDD5A6F008E8EF6 /* NavigationRouterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterApp.swift; sourceTree = ""; }; 56 | A8FCF2592CDD5A70008E8EF6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | A8FCF25C2CDD5A70008E8EF6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 58 | A8FCF2622CDD5A70008E8EF6 /* NavigationRouterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NavigationRouterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | A8FCF2662CDD5A70008E8EF6 /* AppRoutesCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutesCoordinatorTests.swift; sourceTree = ""; }; 60 | A8FCF26C2CDD5A70008E8EF6 /* NavigationRouterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NavigationRouterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | A8FCF2702CDD5A70008E8EF6 /* NavigationRouterUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterUITests.swift; sourceTree = ""; }; 62 | A8FCF2722CDD5A70008E8EF6 /* NavigationRouterUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterUITestsLaunchTests.swift; sourceTree = ""; }; 63 | A8FCF2802CDD5A9B008E8EF6 /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; 64 | A8FCF2832CDD5AB8008E8EF6 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; 65 | A8FCF28B2CDD5B00008E8EF6 /* IssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueView.swift; sourceTree = ""; }; 66 | A8FCF28D2CDD5B20008E8EF6 /* FavouritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouritesView.swift; sourceTree = ""; }; 67 | A8FCF28F2CDD5B31008E8EF6 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; 68 | A8FCF2912CDD5D90008E8EF6 /* ArticlesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesView.swift; sourceTree = ""; }; 69 | A8FCF2932CDD5F6F008E8EF6 /* Issue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Issue.swift; sourceTree = ""; }; 70 | A8FCF2982CDD6139008E8EF6 /* FavouritesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouritesManager.swift; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | A8FCF24F2CDD5A6F008E8EF6 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | A8FCF25F2CDD5A70008E8EF6 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | A8FCF2692CDD5A70008E8EF6 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXFrameworksBuildPhase section */ 96 | 97 | /* Begin PBXGroup section */ 98 | A8510BC72CDF6B8200EC1BA4 /* Demo */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | A8510BC82CDF6B9700EC1BA4 /* router_navigation_demo.gif */, 102 | ); 103 | path = Demo; 104 | sourceTree = ""; 105 | }; 106 | A8FCF2492CDD5A6F008E8EF6 = { 107 | isa = PBXGroup; 108 | children = ( 109 | A8FCF2542CDD5A6F008E8EF6 /* NavigationRouter */, 110 | A8FCF2652CDD5A70008E8EF6 /* NavigationRouterTests */, 111 | A8FCF26F2CDD5A70008E8EF6 /* NavigationRouterUITests */, 112 | A8FCF2532CDD5A6F008E8EF6 /* Products */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | A8FCF2532CDD5A6F008E8EF6 /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | A8FCF2522CDD5A6F008E8EF6 /* NavigationRouter.app */, 120 | A8FCF2622CDD5A70008E8EF6 /* NavigationRouterTests.xctest */, 121 | A8FCF26C2CDD5A70008E8EF6 /* NavigationRouterUITests.xctest */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | A8FCF2542CDD5A6F008E8EF6 /* NavigationRouter */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | A8510BC72CDF6B8200EC1BA4 /* Demo */, 130 | A8FCF2972CDD6123008E8EF6 /* Managers */, 131 | A8FCF28A2CDD5AF3008E8EF6 /* Views */, 132 | A8FCF2852CDD5ACD008E8EF6 /* Coordinator */, 133 | A8FCF2822CDD5AAC008E8EF6 /* Extensions */, 134 | A8FCF27F2CDD5A8F008E8EF6 /* Models */, 135 | A8FCF2552CDD5A6F008E8EF6 /* NavigationRouterApp.swift */, 136 | A8FCF2592CDD5A70008E8EF6 /* Assets.xcassets */, 137 | A8FCF25B2CDD5A70008E8EF6 /* Preview Content */, 138 | ); 139 | path = NavigationRouter; 140 | sourceTree = ""; 141 | }; 142 | A8FCF25B2CDD5A70008E8EF6 /* Preview Content */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | A8FCF25C2CDD5A70008E8EF6 /* Preview Assets.xcassets */, 146 | ); 147 | path = "Preview Content"; 148 | sourceTree = ""; 149 | }; 150 | A8FCF2652CDD5A70008E8EF6 /* NavigationRouterTests */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | A8FCF2662CDD5A70008E8EF6 /* AppRoutesCoordinatorTests.swift */, 154 | ); 155 | path = NavigationRouterTests; 156 | sourceTree = ""; 157 | }; 158 | A8FCF26F2CDD5A70008E8EF6 /* NavigationRouterUITests */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | A8FCF2702CDD5A70008E8EF6 /* NavigationRouterUITests.swift */, 162 | A8FCF2722CDD5A70008E8EF6 /* NavigationRouterUITestsLaunchTests.swift */, 163 | ); 164 | path = NavigationRouterUITests; 165 | sourceTree = ""; 166 | }; 167 | A8FCF27F2CDD5A8F008E8EF6 /* Models */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | A8FCF2802CDD5A9B008E8EF6 /* Article.swift */, 171 | A8FCF2932CDD5F6F008E8EF6 /* Issue.swift */, 172 | ); 173 | path = Models; 174 | sourceTree = ""; 175 | }; 176 | A8FCF2822CDD5AAC008E8EF6 /* Extensions */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | A8FCF2832CDD5AB8008E8EF6 /* DateExtensions.swift */, 180 | ); 181 | path = Extensions; 182 | sourceTree = ""; 183 | }; 184 | A8FCF2852CDD5ACD008E8EF6 /* Coordinator */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | A898C34F2CE264A5006ED5B5 /* Routes.swift */, 188 | A898C3482CE260C1006ED5B5 /* Routable.swift */, 189 | A898C34A2CE26147006ED5B5 /* Coordinator.swift */, 190 | A898C34C2CE26336006ED5B5 /* CoordinatorStack.swift */, 191 | ); 192 | path = Coordinator; 193 | sourceTree = ""; 194 | }; 195 | A8FCF28A2CDD5AF3008E8EF6 /* Views */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | A8FCF28B2CDD5B00008E8EF6 /* IssueView.swift */, 199 | A8FCF28D2CDD5B20008E8EF6 /* FavouritesView.swift */, 200 | A8FCF28F2CDD5B31008E8EF6 /* AppView.swift */, 201 | A8FCF2912CDD5D90008E8EF6 /* ArticlesView.swift */, 202 | ); 203 | path = Views; 204 | sourceTree = ""; 205 | }; 206 | A8FCF2972CDD6123008E8EF6 /* Managers */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | A8FCF2982CDD6139008E8EF6 /* FavouritesManager.swift */, 210 | ); 211 | path = Managers; 212 | sourceTree = ""; 213 | }; 214 | /* End PBXGroup section */ 215 | 216 | /* Begin PBXNativeTarget section */ 217 | A8FCF2512CDD5A6F008E8EF6 /* NavigationRouter */ = { 218 | isa = PBXNativeTarget; 219 | buildConfigurationList = A8FCF2762CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouter" */; 220 | buildPhases = ( 221 | A8FCF24E2CDD5A6F008E8EF6 /* Sources */, 222 | A8FCF24F2CDD5A6F008E8EF6 /* Frameworks */, 223 | A8FCF2502CDD5A6F008E8EF6 /* Resources */, 224 | ); 225 | buildRules = ( 226 | ); 227 | dependencies = ( 228 | ); 229 | name = NavigationRouter; 230 | productName = NavigationRouter; 231 | productReference = A8FCF2522CDD5A6F008E8EF6 /* NavigationRouter.app */; 232 | productType = "com.apple.product-type.application"; 233 | }; 234 | A8FCF2612CDD5A70008E8EF6 /* NavigationRouterTests */ = { 235 | isa = PBXNativeTarget; 236 | buildConfigurationList = A8FCF2792CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouterTests" */; 237 | buildPhases = ( 238 | A8FCF25E2CDD5A70008E8EF6 /* Sources */, 239 | A8FCF25F2CDD5A70008E8EF6 /* Frameworks */, 240 | A8FCF2602CDD5A70008E8EF6 /* Resources */, 241 | ); 242 | buildRules = ( 243 | ); 244 | dependencies = ( 245 | A8FCF2642CDD5A70008E8EF6 /* PBXTargetDependency */, 246 | ); 247 | name = NavigationRouterTests; 248 | productName = NavigationRouterTests; 249 | productReference = A8FCF2622CDD5A70008E8EF6 /* NavigationRouterTests.xctest */; 250 | productType = "com.apple.product-type.bundle.unit-test"; 251 | }; 252 | A8FCF26B2CDD5A70008E8EF6 /* NavigationRouterUITests */ = { 253 | isa = PBXNativeTarget; 254 | buildConfigurationList = A8FCF27C2CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouterUITests" */; 255 | buildPhases = ( 256 | A8FCF2682CDD5A70008E8EF6 /* Sources */, 257 | A8FCF2692CDD5A70008E8EF6 /* Frameworks */, 258 | A8FCF26A2CDD5A70008E8EF6 /* Resources */, 259 | ); 260 | buildRules = ( 261 | ); 262 | dependencies = ( 263 | A8FCF26E2CDD5A70008E8EF6 /* PBXTargetDependency */, 264 | ); 265 | name = NavigationRouterUITests; 266 | productName = NavigationRouterUITests; 267 | productReference = A8FCF26C2CDD5A70008E8EF6 /* NavigationRouterUITests.xctest */; 268 | productType = "com.apple.product-type.bundle.ui-testing"; 269 | }; 270 | /* End PBXNativeTarget section */ 271 | 272 | /* Begin PBXProject section */ 273 | A8FCF24A2CDD5A6F008E8EF6 /* Project object */ = { 274 | isa = PBXProject; 275 | attributes = { 276 | BuildIndependentTargetsInParallel = 1; 277 | LastSwiftUpdateCheck = 1540; 278 | LastUpgradeCheck = 1540; 279 | TargetAttributes = { 280 | A8FCF2512CDD5A6F008E8EF6 = { 281 | CreatedOnToolsVersion = 15.4; 282 | }; 283 | A8FCF2612CDD5A70008E8EF6 = { 284 | CreatedOnToolsVersion = 15.4; 285 | TestTargetID = A8FCF2512CDD5A6F008E8EF6; 286 | }; 287 | A8FCF26B2CDD5A70008E8EF6 = { 288 | CreatedOnToolsVersion = 15.4; 289 | TestTargetID = A8FCF2512CDD5A6F008E8EF6; 290 | }; 291 | }; 292 | }; 293 | buildConfigurationList = A8FCF24D2CDD5A6F008E8EF6 /* Build configuration list for PBXProject "NavigationRouter" */; 294 | compatibilityVersion = "Xcode 14.0"; 295 | developmentRegion = en; 296 | hasScannedForEncodings = 0; 297 | knownRegions = ( 298 | en, 299 | Base, 300 | ); 301 | mainGroup = A8FCF2492CDD5A6F008E8EF6; 302 | productRefGroup = A8FCF2532CDD5A6F008E8EF6 /* Products */; 303 | projectDirPath = ""; 304 | projectRoot = ""; 305 | targets = ( 306 | A8FCF2512CDD5A6F008E8EF6 /* NavigationRouter */, 307 | A8FCF2612CDD5A70008E8EF6 /* NavigationRouterTests */, 308 | A8FCF26B2CDD5A70008E8EF6 /* NavigationRouterUITests */, 309 | ); 310 | }; 311 | /* End PBXProject section */ 312 | 313 | /* Begin PBXResourcesBuildPhase section */ 314 | A8FCF2502CDD5A6F008E8EF6 /* Resources */ = { 315 | isa = PBXResourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | A8FCF25D2CDD5A70008E8EF6 /* Preview Assets.xcassets in Resources */, 319 | A8FCF25A2CDD5A70008E8EF6 /* Assets.xcassets in Resources */, 320 | A8510BC92CDF6B9700EC1BA4 /* router_navigation_demo.gif in Resources */, 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | A8FCF2602CDD5A70008E8EF6 /* Resources */ = { 325 | isa = PBXResourcesBuildPhase; 326 | buildActionMask = 2147483647; 327 | files = ( 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | A8FCF26A2CDD5A70008E8EF6 /* Resources */ = { 332 | isa = PBXResourcesBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | ); 336 | runOnlyForDeploymentPostprocessing = 0; 337 | }; 338 | /* End PBXResourcesBuildPhase section */ 339 | 340 | /* Begin PBXSourcesBuildPhase section */ 341 | A8FCF24E2CDD5A6F008E8EF6 /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | A8FCF2812CDD5A9B008E8EF6 /* Article.swift in Sources */, 346 | A8FCF2942CDD5F6F008E8EF6 /* Issue.swift in Sources */, 347 | A898C34B2CE26147006ED5B5 /* Coordinator.swift in Sources */, 348 | A8FCF28C2CDD5B00008E8EF6 /* IssueView.swift in Sources */, 349 | A8FCF2902CDD5B31008E8EF6 /* AppView.swift in Sources */, 350 | A8FCF28E2CDD5B20008E8EF6 /* FavouritesView.swift in Sources */, 351 | A898C3492CE260C1006ED5B5 /* Routable.swift in Sources */, 352 | A8FCF2842CDD5AB8008E8EF6 /* DateExtensions.swift in Sources */, 353 | A8FCF2992CDD6139008E8EF6 /* FavouritesManager.swift in Sources */, 354 | A8FCF2922CDD5D90008E8EF6 /* ArticlesView.swift in Sources */, 355 | A8FCF2562CDD5A6F008E8EF6 /* NavigationRouterApp.swift in Sources */, 356 | A898C3502CE264A5006ED5B5 /* Routes.swift in Sources */, 357 | A898C34D2CE26336006ED5B5 /* CoordinatorStack.swift in Sources */, 358 | ); 359 | runOnlyForDeploymentPostprocessing = 0; 360 | }; 361 | A8FCF25E2CDD5A70008E8EF6 /* Sources */ = { 362 | isa = PBXSourcesBuildPhase; 363 | buildActionMask = 2147483647; 364 | files = ( 365 | A8FCF2672CDD5A70008E8EF6 /* AppRoutesCoordinatorTests.swift in Sources */, 366 | ); 367 | runOnlyForDeploymentPostprocessing = 0; 368 | }; 369 | A8FCF2682CDD5A70008E8EF6 /* Sources */ = { 370 | isa = PBXSourcesBuildPhase; 371 | buildActionMask = 2147483647; 372 | files = ( 373 | A8FCF2712CDD5A70008E8EF6 /* NavigationRouterUITests.swift in Sources */, 374 | A8FCF2732CDD5A70008E8EF6 /* NavigationRouterUITestsLaunchTests.swift in Sources */, 375 | ); 376 | runOnlyForDeploymentPostprocessing = 0; 377 | }; 378 | /* End PBXSourcesBuildPhase section */ 379 | 380 | /* Begin PBXTargetDependency section */ 381 | A8FCF2642CDD5A70008E8EF6 /* PBXTargetDependency */ = { 382 | isa = PBXTargetDependency; 383 | target = A8FCF2512CDD5A6F008E8EF6 /* NavigationRouter */; 384 | targetProxy = A8FCF2632CDD5A70008E8EF6 /* PBXContainerItemProxy */; 385 | }; 386 | A8FCF26E2CDD5A70008E8EF6 /* PBXTargetDependency */ = { 387 | isa = PBXTargetDependency; 388 | target = A8FCF2512CDD5A6F008E8EF6 /* NavigationRouter */; 389 | targetProxy = A8FCF26D2CDD5A70008E8EF6 /* PBXContainerItemProxy */; 390 | }; 391 | /* End PBXTargetDependency section */ 392 | 393 | /* Begin XCBuildConfiguration section */ 394 | A8FCF2742CDD5A70008E8EF6 /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 399 | CLANG_ANALYZER_NONNULL = YES; 400 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 401 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 402 | CLANG_ENABLE_MODULES = YES; 403 | CLANG_ENABLE_OBJC_ARC = YES; 404 | CLANG_ENABLE_OBJC_WEAK = YES; 405 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 406 | CLANG_WARN_BOOL_CONVERSION = YES; 407 | CLANG_WARN_COMMA = YES; 408 | CLANG_WARN_CONSTANT_CONVERSION = YES; 409 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 410 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 411 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 422 | CLANG_WARN_STRICT_PROTOTYPES = YES; 423 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 424 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 425 | CLANG_WARN_UNREACHABLE_CODE = YES; 426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 427 | COPY_PHASE_STRIP = NO; 428 | DEBUG_INFORMATION_FORMAT = dwarf; 429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 430 | ENABLE_TESTABILITY = YES; 431 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 432 | GCC_C_LANGUAGE_STANDARD = gnu17; 433 | GCC_DYNAMIC_NO_PIC = NO; 434 | GCC_NO_COMMON_BLOCKS = YES; 435 | GCC_OPTIMIZATION_LEVEL = 0; 436 | GCC_PREPROCESSOR_DEFINITIONS = ( 437 | "DEBUG=1", 438 | "$(inherited)", 439 | ); 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 447 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 448 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 449 | MTL_FAST_MATH = YES; 450 | ONLY_ACTIVE_ARCH = YES; 451 | SDKROOT = iphoneos; 452 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 453 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 454 | }; 455 | name = Debug; 456 | }; 457 | A8FCF2752CDD5A70008E8EF6 /* Release */ = { 458 | isa = XCBuildConfiguration; 459 | buildSettings = { 460 | ALWAYS_SEARCH_USER_PATHS = NO; 461 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 462 | CLANG_ANALYZER_NONNULL = YES; 463 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 464 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 465 | CLANG_ENABLE_MODULES = YES; 466 | CLANG_ENABLE_OBJC_ARC = YES; 467 | CLANG_ENABLE_OBJC_WEAK = YES; 468 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 469 | CLANG_WARN_BOOL_CONVERSION = YES; 470 | CLANG_WARN_COMMA = YES; 471 | CLANG_WARN_CONSTANT_CONVERSION = YES; 472 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 473 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 474 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 475 | CLANG_WARN_EMPTY_BODY = YES; 476 | CLANG_WARN_ENUM_CONVERSION = YES; 477 | CLANG_WARN_INFINITE_RECURSION = YES; 478 | CLANG_WARN_INT_CONVERSION = YES; 479 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 480 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 481 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 482 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 483 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 484 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 485 | CLANG_WARN_STRICT_PROTOTYPES = YES; 486 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 487 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 488 | CLANG_WARN_UNREACHABLE_CODE = YES; 489 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 490 | COPY_PHASE_STRIP = NO; 491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 492 | ENABLE_NS_ASSERTIONS = NO; 493 | ENABLE_STRICT_OBJC_MSGSEND = YES; 494 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 495 | GCC_C_LANGUAGE_STANDARD = gnu17; 496 | GCC_NO_COMMON_BLOCKS = YES; 497 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 498 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 499 | GCC_WARN_UNDECLARED_SELECTOR = YES; 500 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 501 | GCC_WARN_UNUSED_FUNCTION = YES; 502 | GCC_WARN_UNUSED_VARIABLE = YES; 503 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 504 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 505 | MTL_ENABLE_DEBUG_INFO = NO; 506 | MTL_FAST_MATH = YES; 507 | SDKROOT = iphoneos; 508 | SWIFT_COMPILATION_MODE = wholemodule; 509 | VALIDATE_PRODUCT = YES; 510 | }; 511 | name = Release; 512 | }; 513 | A8FCF2772CDD5A70008E8EF6 /* Debug */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 517 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 518 | CODE_SIGN_STYLE = Automatic; 519 | CURRENT_PROJECT_VERSION = 1; 520 | DEVELOPMENT_ASSET_PATHS = "\"NavigationRouter/Preview Content\""; 521 | DEVELOPMENT_TEAM = P82VY4LSH8; 522 | ENABLE_PREVIEWS = YES; 523 | GENERATE_INFOPLIST_FILE = YES; 524 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 525 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 526 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 527 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 528 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 529 | LD_RUNPATH_SEARCH_PATHS = ( 530 | "$(inherited)", 531 | "@executable_path/Frameworks", 532 | ); 533 | MARKETING_VERSION = 1.0; 534 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouter; 535 | PRODUCT_NAME = "$(TARGET_NAME)"; 536 | SWIFT_EMIT_LOC_STRINGS = YES; 537 | SWIFT_VERSION = 5.0; 538 | TARGETED_DEVICE_FAMILY = "1,2"; 539 | }; 540 | name = Debug; 541 | }; 542 | A8FCF2782CDD5A70008E8EF6 /* Release */ = { 543 | isa = XCBuildConfiguration; 544 | buildSettings = { 545 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 546 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 547 | CODE_SIGN_STYLE = Automatic; 548 | CURRENT_PROJECT_VERSION = 1; 549 | DEVELOPMENT_ASSET_PATHS = "\"NavigationRouter/Preview Content\""; 550 | DEVELOPMENT_TEAM = P82VY4LSH8; 551 | ENABLE_PREVIEWS = YES; 552 | GENERATE_INFOPLIST_FILE = YES; 553 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 554 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 555 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 556 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 557 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 558 | LD_RUNPATH_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "@executable_path/Frameworks", 561 | ); 562 | MARKETING_VERSION = 1.0; 563 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouter; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | SWIFT_EMIT_LOC_STRINGS = YES; 566 | SWIFT_VERSION = 5.0; 567 | TARGETED_DEVICE_FAMILY = "1,2"; 568 | }; 569 | name = Release; 570 | }; 571 | A8FCF27A2CDD5A70008E8EF6 /* Debug */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 575 | BUNDLE_LOADER = "$(TEST_HOST)"; 576 | CODE_SIGN_STYLE = Automatic; 577 | CURRENT_PROJECT_VERSION = 1; 578 | DEVELOPMENT_TEAM = P82VY4LSH8; 579 | GENERATE_INFOPLIST_FILE = YES; 580 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 581 | MARKETING_VERSION = 1.0; 582 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouterTests; 583 | PRODUCT_NAME = "$(TARGET_NAME)"; 584 | SWIFT_EMIT_LOC_STRINGS = NO; 585 | SWIFT_VERSION = 5.0; 586 | TARGETED_DEVICE_FAMILY = "1,2"; 587 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NavigationRouter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NavigationRouter"; 588 | }; 589 | name = Debug; 590 | }; 591 | A8FCF27B2CDD5A70008E8EF6 /* Release */ = { 592 | isa = XCBuildConfiguration; 593 | buildSettings = { 594 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 595 | BUNDLE_LOADER = "$(TEST_HOST)"; 596 | CODE_SIGN_STYLE = Automatic; 597 | CURRENT_PROJECT_VERSION = 1; 598 | DEVELOPMENT_TEAM = P82VY4LSH8; 599 | GENERATE_INFOPLIST_FILE = YES; 600 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 601 | MARKETING_VERSION = 1.0; 602 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouterTests; 603 | PRODUCT_NAME = "$(TARGET_NAME)"; 604 | SWIFT_EMIT_LOC_STRINGS = NO; 605 | SWIFT_VERSION = 5.0; 606 | TARGETED_DEVICE_FAMILY = "1,2"; 607 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NavigationRouter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NavigationRouter"; 608 | }; 609 | name = Release; 610 | }; 611 | A8FCF27D2CDD5A70008E8EF6 /* Debug */ = { 612 | isa = XCBuildConfiguration; 613 | buildSettings = { 614 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 615 | CODE_SIGN_STYLE = Automatic; 616 | CURRENT_PROJECT_VERSION = 1; 617 | DEVELOPMENT_TEAM = P82VY4LSH8; 618 | GENERATE_INFOPLIST_FILE = YES; 619 | MARKETING_VERSION = 1.0; 620 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouterUITests; 621 | PRODUCT_NAME = "$(TARGET_NAME)"; 622 | SWIFT_EMIT_LOC_STRINGS = NO; 623 | SWIFT_VERSION = 5.0; 624 | TARGETED_DEVICE_FAMILY = "1,2"; 625 | TEST_TARGET_NAME = NavigationRouter; 626 | }; 627 | name = Debug; 628 | }; 629 | A8FCF27E2CDD5A70008E8EF6 /* Release */ = { 630 | isa = XCBuildConfiguration; 631 | buildSettings = { 632 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 633 | CODE_SIGN_STYLE = Automatic; 634 | CURRENT_PROJECT_VERSION = 1; 635 | DEVELOPMENT_TEAM = P82VY4LSH8; 636 | GENERATE_INFOPLIST_FILE = YES; 637 | MARKETING_VERSION = 1.0; 638 | PRODUCT_BUNDLE_IDENTIFIER = com.tiago.henriques.NavigationRouterUITests; 639 | PRODUCT_NAME = "$(TARGET_NAME)"; 640 | SWIFT_EMIT_LOC_STRINGS = NO; 641 | SWIFT_VERSION = 5.0; 642 | TARGETED_DEVICE_FAMILY = "1,2"; 643 | TEST_TARGET_NAME = NavigationRouter; 644 | }; 645 | name = Release; 646 | }; 647 | /* End XCBuildConfiguration section */ 648 | 649 | /* Begin XCConfigurationList section */ 650 | A8FCF24D2CDD5A6F008E8EF6 /* Build configuration list for PBXProject "NavigationRouter" */ = { 651 | isa = XCConfigurationList; 652 | buildConfigurations = ( 653 | A8FCF2742CDD5A70008E8EF6 /* Debug */, 654 | A8FCF2752CDD5A70008E8EF6 /* Release */, 655 | ); 656 | defaultConfigurationIsVisible = 0; 657 | defaultConfigurationName = Release; 658 | }; 659 | A8FCF2762CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouter" */ = { 660 | isa = XCConfigurationList; 661 | buildConfigurations = ( 662 | A8FCF2772CDD5A70008E8EF6 /* Debug */, 663 | A8FCF2782CDD5A70008E8EF6 /* Release */, 664 | ); 665 | defaultConfigurationIsVisible = 0; 666 | defaultConfigurationName = Release; 667 | }; 668 | A8FCF2792CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouterTests" */ = { 669 | isa = XCConfigurationList; 670 | buildConfigurations = ( 671 | A8FCF27A2CDD5A70008E8EF6 /* Debug */, 672 | A8FCF27B2CDD5A70008E8EF6 /* Release */, 673 | ); 674 | defaultConfigurationIsVisible = 0; 675 | defaultConfigurationName = Release; 676 | }; 677 | A8FCF27C2CDD5A70008E8EF6 /* Build configuration list for PBXNativeTarget "NavigationRouterUITests" */ = { 678 | isa = XCConfigurationList; 679 | buildConfigurations = ( 680 | A8FCF27D2CDD5A70008E8EF6 /* Debug */, 681 | A8FCF27E2CDD5A70008E8EF6 /* Release */, 682 | ); 683 | defaultConfigurationIsVisible = 0; 684 | defaultConfigurationName = Release; 685 | }; 686 | /* End XCConfigurationList section */ 687 | }; 688 | rootObject = A8FCF24A2CDD5A6F008E8EF6 /* Project object */; 689 | } 690 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.xcworkspace/xcuserdata/tiagohenriques.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henriquestiagoo/navigation-coordinator-swiftui/7fdde68f5fe35e49c3144a36afb5dbfefb4617d7/NavigationRouter.xcodeproj/project.xcworkspace/xcuserdata/tiagohenriques.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/xcuserdata/tiagohenriques.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NavigationRouter.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NavigationRouter/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NavigationRouter/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NavigationRouter/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NavigationRouter/Coordinator/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 11/11/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @Observable 12 | class Coordinator { 13 | 14 | var path: NavigationPath = NavigationPath() 15 | var sheet: Route? 16 | var fullscreenCover: Route? 17 | 18 | enum NavigationType { 19 | case push 20 | case sheet 21 | case fullScreenCover 22 | } 23 | 24 | enum NavigationPopType { 25 | case push(last: Int) 26 | case sheet 27 | case fullScreenCover 28 | } 29 | 30 | func push(_ page: Route, type: NavigationType = .push) { 31 | switch type { 32 | case .push: 33 | path.append(page) 34 | case .sheet: 35 | sheet = page 36 | case .fullScreenCover: 37 | fullscreenCover = page 38 | } 39 | } 40 | 41 | func pop(_ type: NavigationPopType = .push(last: 1)) { 42 | switch type { 43 | case .push(let last): 44 | path.removeLast(last) 45 | case .sheet: 46 | sheet = nil 47 | case .fullScreenCover: 48 | fullscreenCover = nil 49 | } 50 | } 51 | 52 | func reset() { 53 | path.removeLast(path.count) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /NavigationRouter/Coordinator/CoordinatorStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorStack.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 11/11/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct CoordinatorStack: View { 12 | let root: Route 13 | @State private var coordinator = Coordinator() 14 | 15 | init(root: Route) { 16 | self.root = root 17 | } 18 | 19 | var body: some View { 20 | NavigationStack(path: $coordinator.path) { 21 | root 22 | .navigationDestination(for: Route.self) { $0 } 23 | .sheet(item: $coordinator.sheet) { $0 } 24 | .fullScreenCover(item: $coordinator.fullscreenCover) { $0 } 25 | } 26 | .environment(coordinator) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NavigationRouter/Coordinator/Routable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinatable.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 11/11/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | protocol Routable: View, Hashable, Identifiable {} 12 | -------------------------------------------------------------------------------- /NavigationRouter/Coordinator/Routes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Routes.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 11/11/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum AppRoutes: Routable { 12 | var id: UUID { UUID() } 13 | 14 | case articles(articles: [Article]) // article model needs to conform to Hashable. 15 | case favourites 16 | case issue(issue: Issue) // issue model needs to conform to Hashable. 17 | case root 18 | 19 | var body: some View { 20 | switch self { 21 | case .articles(let articles): 22 | ArticlesView(articles: articles) 23 | case .favourites: 24 | CoordinatorStack(root: FavouritesRoutes.root) 25 | case .issue(let issue): 26 | IssueView(issue: issue) 27 | case .root: 28 | AppView() 29 | } 30 | } 31 | } 32 | 33 | enum FavouritesRoutes: Routable { 34 | var id: UUID { UUID() } 35 | 36 | case root 37 | 38 | var body: some View { 39 | switch self { 40 | case .root: 41 | FavouritesView() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NavigationRouter/Demo/router_navigation_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henriquestiagoo/navigation-coordinator-swiftui/7fdde68f5fe35e49c3144a36afb5dbfefb4617d7/NavigationRouter/Demo/router_navigation_demo.gif -------------------------------------------------------------------------------- /NavigationRouter/Extensions/DateExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateExtensions.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | static func from(year: Int, month: Int, day: Int) -> Date { 12 | let calendar = Calendar.current 13 | var dateComponents = DateComponents() 14 | dateComponents.year = year 15 | dateComponents.month = month 16 | dateComponents.day = day 17 | return calendar.date(from: dateComponents) ?? .init() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NavigationRouter/Managers/FavouritesManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavouritesManager.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FavouritesManager: ObservableObject { 11 | @Published private(set) var issues: [Issue] = [] 12 | 13 | public init(issues: [Issue] = []) { 14 | self.issues = issues 15 | } 16 | 17 | func add(_ item: Issue) { 18 | issues.append(item) 19 | } 20 | 21 | func remove(id: Int) { 22 | issues.removeAll(where: { $0.id == id }) 23 | } 24 | 25 | func contains(_ issue: Issue) -> Bool { 26 | return issues.contains(where: { $0.id == issue.id }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NavigationRouter/Models/Article.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Article: Hashable, Identifiable, Equatable { 11 | let id: Int 12 | let title: String 13 | let url: String 14 | } 15 | 16 | extension Article { 17 | static let articlesFromIssue18: [Article] = [ 18 | .init( 19 | id: 0, 20 | title: "🏎 Working at startups vs large companies", 21 | url: "https://swiftrocks.com/working-at-startups-vs-large-companies" 22 | ), 23 | .init( 24 | id: 1, 25 | title: "⚠️ Why Do View Controllers Need init(coder:)?", 26 | url: "https://codingwithvera.com/why-do-uiviewcontrollers-need-init-coder/" 27 | ), 28 | .init( 29 | id: 2, 30 | title: "🧱 The perfect iOS networking layer does not exist - Part 2", 31 | url: "https://calincrist.com/the-perfect-ios-networking-layer-does-not-exist---part-2" 32 | ) 33 | ] 34 | 35 | static let articlesFromIssue19: [Article] = [ 36 | .init( 37 | id: 3, 38 | title: "🛠️ A Different Approach Using the Swift Argument Parser", 39 | url: "https://swifttoolkit.dev/posts/argument-parser-custom" 40 | ), 41 | .init( 42 | id: 4, 43 | title: "🕶 Improve your app's UX with SwiftUI's task view modifier", 44 | url: "https://peterfriese.dev/blog/2024/delay-task-modifier/" 45 | ), 46 | .init( 47 | id: 5, 48 | title: "🚫 Nitpicking during code review is just a waste of time. Invest in tooling instead", 49 | url: "https://medium.com/@SaezChristopher/nitpicking-during-code-review-is-just-a-waste-of-time-invest-in-tooling-instead-07ae29f4a56a" 50 | ) 51 | ] 52 | 53 | static let articlesFromIssue20: [Article] = [ 54 | .init( 55 | id: 6, 56 | title: "🧜‍♀️ The Strategy Pattern", 57 | url: "https://codingwithvera.com/the-strategy-pattern/" 58 | ), 59 | .init( 60 | id: 7, 61 | title: "🚣‍♂️ Coordinators & SwiftUI", 62 | url: "https://vbat.dev/coordinators-swiftui" 63 | ), 64 | .init( 65 | id: 8, 66 | title: "👶 Getting Started with Swift Package Manager", 67 | url: "https://swiftonserver.com/getting-started-with-swift-package-manager/" 68 | ), 69 | .init( 70 | id: 9, 71 | title: "💡 Blog about Swift: Tips and ideas to start your own", 72 | url: "https://www.avanderlee.com/swift/blog-about-swift/" 73 | ) 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /NavigationRouter/Models/Issue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Issue.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Issue: Hashable, Identifiable { 11 | var id: Int { number } 12 | let title: String 13 | let description: String 14 | let number: Int 15 | let image: String 16 | let date: Date 17 | let url: String 18 | let articles: [Article] 19 | } 20 | 21 | extension Issue { 22 | static let mocks: [Issue] = [ 23 | .init( 24 | title: "👀 Reaching (almost) my personal goals for 2024! 🏋🏼‍♀️", 25 | description: "Earlier this year, I set several personal milestones, and as we enter the final quarter, I am happy to say that the only goal I haven't hit yet is going to the gym four times a week 😅🏋🏼‍♀️!", 26 | number: 18, 27 | image: "https://raw.githubusercontent.com/henriquestiagoo/ioscoffeebreak-ci/refs/heads/main/issues/18.png", 28 | date: Date.from(year: 2024, month: 10, day: 20), 29 | url: "https://www.ioscoffeebreak.com/issue/issue18", 30 | articles: Article.articlesFromIssue18 31 | ), 32 | .init( 33 | title: "🥷 Migrating workflows from TeamCity to Xcode Cloud ☁️", 34 | description: "Last week, I have been migrating our CI/CD pipelines from TeamCity to Xcode Cloud.", 35 | number: 19, 36 | image: "https://raw.githubusercontent.com/henriquestiagoo/ioscoffeebreak-ci/refs/heads/main/issues/19.png", 37 | date: Date.from(year: 2024, month: 10, day: 27), 38 | url: "https://www.ioscoffeebreak.com/issue/issue19", 39 | articles: Article.articlesFromIssue19 40 | ), 41 | .init( 42 | title: "🧑‍✈️ GitHub Copilot for Xcode is now available! ✈️", 43 | description: "This week, GitHub revealed that GitHub Copilot's code completion feature is now available in public preview for Xcode!", 44 | number: 20, 45 | image: "https://raw.githubusercontent.com/henriquestiagoo/ioscoffeebreak-ci/refs/heads/main/issues/20.png", 46 | date: Date.from(year: 2024, month: 11, day: 3), 47 | url: "https://www.ioscoffeebreak.com/issue/issue20", 48 | articles: Article.articlesFromIssue20 49 | ) 50 | ] 51 | 52 | static let sortedMocks = mocks.sorted(by: { $0.date > $1.date }) 53 | } 54 | -------------------------------------------------------------------------------- /NavigationRouter/NavigationRouterApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRouterApp.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct NavigationRouterApp: App { 12 | @State private var favouritesManager = FavouritesManager() 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | CoordinatorStack(root: AppRoutes.root) 17 | .environmentObject(favouritesManager) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NavigationRouter/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NavigationRouter/Views/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppView.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppView: View { 11 | @Environment(Coordinator.self) private var appCoordinator 12 | @EnvironmentObject private var favouritesManager: FavouritesManager 13 | 14 | var body: some View { 15 | List { 16 | Section("Issues") { 17 | ForEach(Issue.sortedMocks) { issue in 18 | NavigationLink( 19 | value: AppRoutes.issue(issue: issue) 20 | ) { 21 | IssueRowView(issue: issue) 22 | } 23 | } 24 | } 25 | } 26 | .toolbar { 27 | ToolbarItem(placement: .primaryAction) { 28 | Button { 29 | appCoordinator.push(.favourites, type: .sheet) 30 | } label: { 31 | Image(systemName: "heart") 32 | .symbolVariant(.fill) 33 | .padding(4) 34 | } 35 | } 36 | } 37 | .navigationTitle("☕ iOS Coffee Break") 38 | } 39 | } 40 | 41 | struct IssueRowView: View { 42 | let issue: Issue 43 | 44 | var body: some View { 45 | VStack(alignment: .leading) { 46 | Text(issue.title) 47 | .font(.headline) 48 | Text("Issue #\(issue.number)") 49 | .font(.caption) 50 | .foregroundStyle(.secondary) 51 | } 52 | } 53 | } 54 | 55 | #Preview { 56 | CoordinatorStack(root: AppRoutes.root) 57 | .environment(Coordinator()) 58 | .environmentObject(FavouritesManager()) 59 | } 60 | -------------------------------------------------------------------------------- /NavigationRouter/Views/ArticlesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticlesView.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ArticlesView: View { 11 | @Environment(Coordinator.self) private var appCoordinator 12 | let articles: [Article] 13 | 14 | var body: some View { 15 | List { 16 | ForEach(articles) { article in 17 | Text(article.title) 18 | } 19 | 20 | Section { 21 | Button { 22 | appCoordinator.reset() 23 | } label: { 24 | Text("Go back to Issues") 25 | } 26 | } 27 | } 28 | .navigationTitle("Articles") 29 | } 30 | } 31 | 32 | #Preview { 33 | CoordinatorStack(root: AppRoutes.articles(articles: Article.articlesFromIssue20)) 34 | .environment(Coordinator()) 35 | } 36 | -------------------------------------------------------------------------------- /NavigationRouter/Views/FavouritesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavouritesView.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FavouritesView: View { 11 | @EnvironmentObject private var favouritesManager: FavouritesManager 12 | @Environment(Coordinator.self) private var appCoordinator 13 | @Environment(Coordinator.self) private var favouritesCoordinator 14 | 15 | var body: some View { 16 | List { 17 | if favouritesManager.issues.isEmpty { 18 | Text("You have no favourite issues") 19 | } else { 20 | ForEach(favouritesManager.issues) { issue in 21 | Text(issue.title) 22 | .swipeActions { 23 | Button(role: .destructive) { 24 | favouritesManager.remove(id: issue.id) 25 | } label: { 26 | Image(systemName: "trash") 27 | } 28 | } 29 | } 30 | } 31 | } 32 | .toolbar { 33 | ToolbarItem(placement: .primaryAction) { 34 | Button { 35 | appCoordinator.pop(.sheet) 36 | } label: { 37 | Image(systemName: "xmark.circle.fill") 38 | .symbolVariant(.fill) 39 | .padding(4) 40 | } 41 | } 42 | } 43 | .navigationTitle("Favourites") 44 | } 45 | } 46 | 47 | #Preview("With Favourites") { 48 | NavigationStack { 49 | FavouritesView() 50 | .environmentObject(FavouritesManager(issues: Issue.mocks)) 51 | .environment(Coordinator()) 52 | .environment(Coordinator()) 53 | } 54 | } 55 | 56 | #Preview("Without favorites") { 57 | NavigationStack { 58 | FavouritesView() 59 | .environmentObject(FavouritesManager()) 60 | .environment(Coordinator()) 61 | .environment(Coordinator()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NavigationRouter/Views/IssueView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IssueView.swift 3 | // NavigationRouter 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct IssueView: View { 11 | @Environment(Coordinator.self) private var appCoordinator 12 | @EnvironmentObject private var favouritesManager: FavouritesManager 13 | 14 | let issue: Issue 15 | 16 | var body: some View { 17 | List { 18 | Section { 19 | LabeledContent("Number", value: "\(issue.number)") 20 | LabeledContent("Date", value: issue.date.formatted(date: .numeric, time: .omitted)) 21 | } 22 | 23 | Section("Description") { 24 | Text(issue.description) 25 | } 26 | 27 | if !issue.articles.isEmpty { 28 | Section("Articles") { 29 | NavigationLink( 30 | value: AppRoutes.articles(articles: issue.articles) 31 | ) { 32 | Text("\(issue.articles.count) Articles") 33 | } 34 | } 35 | } 36 | 37 | Link(destination: URL(string: issue.url)!) { 38 | Text("Check issue") 39 | } 40 | 41 | if favouritesManager.contains(issue) { 42 | Section { 43 | Button { 44 | favouritesManager.remove(id: issue.id) 45 | } label: { 46 | Label("Remove From Favourites", systemImage: "heart.slash.fill") 47 | .symbolVariant(.fill) 48 | } 49 | } 50 | 51 | } else { 52 | Section { 53 | Button { 54 | favouritesManager.add(issue) 55 | } label: { 56 | Label("Add to Favourites", systemImage: "heart") 57 | .symbolVariant(.fill) 58 | } 59 | } 60 | } 61 | } 62 | .navigationTitle(issue.title) 63 | } 64 | } 65 | 66 | #Preview { 67 | CoordinatorStack(root: AppRoutes.issue(issue: Issue.mocks.last!)) 68 | .environment(Coordinator()) 69 | .environmentObject(FavouritesManager()) 70 | } 71 | -------------------------------------------------------------------------------- /NavigationRouterTests/AppRoutesCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRoutesCoordinatorTests.swift 3 | // AppRoutesCoordinatorTests 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import XCTest 9 | @testable import NavigationRouter 10 | 11 | final class AppRoutesCoordinatorTests: XCTestCase { 12 | private var sut: Coordinator! 13 | 14 | override func setUp() { 15 | sut = Coordinator() 16 | } 17 | 18 | override func tearDown() { 19 | sut = nil 20 | } 21 | 22 | func test_routes_isEmpty_on_init() { 23 | XCTAssertEqual(sut.path.count, 0, "There should be no routes in the coordinator on init.") 24 | XCTAssertNil(sut.sheet, "There should be no sheet route in the coordinator.") 25 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 26 | } 27 | 28 | func test_push_favorites_has_one_route() { 29 | sut.push(.favourites) 30 | XCTAssertEqual(sut.path.count, 1, "There should be one route in the coordinator.") 31 | XCTAssertNil(sut.sheet, "There should be no sheet route in the coordinator.") 32 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 33 | } 34 | 35 | func test_sheet_favorites_has_one_route() { 36 | sut.push(.favourites, type: .sheet) 37 | XCTAssertEqual(sut.path.count, 0, "There should be no routes in the coordinator path.") 38 | XCTAssertEqual(sut.sheet, AppRoutes.favourites, "There should be an AppRoutes.favourites in the sheet route.") 39 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 40 | } 41 | 42 | func test_push_two_screen_has_two_routes() { 43 | sut.push(.issue(issue: Issue.mocks.first!)) 44 | sut.push(.articles(articles: Article.articlesFromIssue18)) 45 | XCTAssertEqual(sut.path.count, 2, "There should be two routes in the coordinator path.") 46 | XCTAssertNil(sut.sheet, "There should be no sheet route in the coordinator.") 47 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 48 | } 49 | 50 | func test_navigate_back_has_one_route() { 51 | sut.push(.issue(issue: Issue.mocks.first!)) 52 | sut.push(.articles(articles: Article.articlesFromIssue18)) 53 | XCTAssertEqual(sut.path.count, 2, "There should be two routes in the coordinator path.") 54 | sut.pop() 55 | XCTAssertEqual(sut.path.count, 1, "There should be one route in the coordinator path.") 56 | XCTAssertNil(sut.sheet, "There should be no sheet route in the coordinator.") 57 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 58 | } 59 | 60 | func test_reset_has_zero_routes() { 61 | sut.push(.issue(issue: Issue.mocks.first!)) 62 | sut.push(.articles(articles: Article.articlesFromIssue18)) 63 | XCTAssertEqual(sut.path.count, 2, "There should be two routes in the coordinator path.") 64 | sut.reset() 65 | XCTAssertEqual(sut.path.count, 0, "There should be no routes in the coordinator path.") 66 | XCTAssertNil(sut.sheet, "There should be no sheet route in the coordinator.") 67 | XCTAssertNil(sut.fullscreenCover, "There should be no fullScreenCover route in the coordinator.") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NavigationRouterUITests/NavigationRouterUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRouterUITests.swift 3 | // NavigationRouterUITests 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class NavigationRouterUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NavigationRouterUITests/NavigationRouterUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRouterUITestsLaunchTests.swift 3 | // NavigationRouterUITests 4 | // 5 | // Created by Tiago Henriques on 07/11/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class NavigationRouterUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI Navigation using the Coordinator Pattern 2 | 3 | Sample project for the article [Refactoring my SwiftUI Navigation Layer to follow the Coordinator Pattern 🔀](https://tiagohenriques.vercel.app/blog/swiftui-refactor-navigation-layer-using-coordinator-pattern), demonstrating how to create a scalable routing system in SwiftUI. 4 |
5 | 6 |

7 | 8 |

9 | --------------------------------------------------------------------------------