├── LICENSE ├── Project ├── Introduction to NavigationStack.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── tunde.adegoroye.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Introduction to NavigationStack.xcscheme │ └── xcuserdata │ │ └── tunde.adegoroye.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── Introduction to NavigationStack │ ├── Allergies │ │ └── Views │ │ │ └── AllergiesDetailView.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── Logo.png │ │ └── Contents.json │ ├── Cart │ │ ├── Manager │ │ │ └── ShoppingCartManager.swift │ │ └── Views │ │ │ ├── CartButton.swift │ │ │ └── CartView.swift │ ├── Desserts │ │ └── Views │ │ │ └── DessertDetailView.swift │ ├── Drinks │ │ └── Views │ │ │ └── DrinkDetailView.swift │ ├── Extensions │ │ └── SwiftUIPreviews+Devices.swift │ ├── Food │ │ └── Views │ │ │ └── FoodDetailView.swift │ ├── Ingredients │ │ └── Views │ │ │ └── IngredientsDetailView.swift │ ├── Introduction to NavigationStack.entitlements │ ├── Introduction_to_NavigationStackApp.swift │ ├── InvalidProductView.swift │ ├── Locations │ │ └── Views │ │ │ ├── LocationMapView.swift │ │ │ └── LocationsDetailView.swift │ ├── Menu │ │ ├── Models │ │ │ └── Food.swift │ │ └── Views │ │ │ ├── MenuCardView.swift │ │ │ ├── MenuGridView.swift │ │ │ ├── MenuItemView.swift │ │ │ ├── MenuListView.swift │ │ │ ├── MenuView.swift │ │ │ ├── ThreeColumnMenuView.swift │ │ │ └── TwoColumnMenuView.swift │ ├── NavigationRouter.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ProductsFetcher.swift │ ├── Promo │ │ └── Views │ │ │ └── PromoView.swift │ ├── Route.swift │ ├── RouteFinder.swift │ ├── Settings │ │ ├── Model │ │ │ └── LayoutExperienceSetting.swift │ │ ├── NotificationsManager.swift │ │ └── View │ │ │ ├── LayoutExperienceView.swift │ │ │ └── SettingsView.swift │ └── Sidebar │ │ ├── Model │ │ └── MenuCategory.swift │ │ └── View │ │ └── SideBarView.swift ├── Introduction to NavigationStackTests │ ├── Introduction_to_NavigationStackTests.swift │ ├── RouteFinderTests.swift │ └── RouteModelTests.swift └── Introduction-to-NavigationStack-Info.plist └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tunde Adegoroye 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 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E702AE662991625E00ABE61B /* CartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E702AE652991625E00ABE61B /* CartButton.swift */; }; 11 | E702AE68299166E100ABE61B /* ShoppingCartManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E702AE67299166E100ABE61B /* ShoppingCartManager.swift */; }; 12 | E702AE6A2991793600ABE61B /* IngredientsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E702AE692991793600ABE61B /* IngredientsDetailView.swift */; }; 13 | E702AE6C29917AAC00ABE61B /* AllergiesDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E702AE6B29917AAC00ABE61B /* AllergiesDetailView.swift */; }; 14 | E70D37CD2992AD00002BEB34 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70D37CC2992AD00002BEB34 /* NavigationRouter.swift */; }; 15 | E70D37CF29939660002BEB34 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70D37CE29939660002BEB34 /* Route.swift */; }; 16 | E70D37D72993A1D9002BEB34 /* Introduction_to_NavigationStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70D37D62993A1D9002BEB34 /* Introduction_to_NavigationStackTests.swift */; }; 17 | E70D37DE2993A353002BEB34 /* RouteModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70D37DD2993A353002BEB34 /* RouteModelTests.swift */; }; 18 | E72C7F8929B7E89C0070B26D /* ThreeColumnMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72C7F8829B7E89C0070B26D /* ThreeColumnMenuView.swift */; }; 19 | E72C83B5298E6C1E0020DB0A /* DrinkDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72C83B4298E6C1E0020DB0A /* DrinkDetailView.swift */; }; 20 | E72C83B7298E6F7E0020DB0A /* DessertDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72C83B6298E6F7E0020DB0A /* DessertDetailView.swift */; }; 21 | E733409F29917C9E00F8B945 /* LocationsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E733409E29917C9E00F8B945 /* LocationsDetailView.swift */; }; 22 | E73340A129917DAC00F8B945 /* LocationMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73340A029917DAC00F8B945 /* LocationMapView.swift */; }; 23 | E73340A32991852400F8B945 /* CartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73340A22991852400F8B945 /* CartView.swift */; }; 24 | E734AA7029959FEF000B8085 /* RouteFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E734AA6F29959FEF000B8085 /* RouteFinder.swift */; }; 25 | E73F8D752995780B003EEBC4 /* ProductsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73F8D742995780B003EEBC4 /* ProductsFetcher.swift */; }; 26 | E75159A6298D680800CDEC3E /* Introduction_to_NavigationStackApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75159A5298D680800CDEC3E /* Introduction_to_NavigationStackApp.swift */; }; 27 | E75159A8298D680800CDEC3E /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75159A7298D680800CDEC3E /* MenuView.swift */; }; 28 | E75159AA298D680A00CDEC3E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E75159A9298D680A00CDEC3E /* Assets.xcassets */; }; 29 | E75159AD298D680A00CDEC3E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E75159AC298D680A00CDEC3E /* Preview Assets.xcassets */; }; 30 | E7520BA62997DE6900752BF1 /* RouteFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7520BA52997DE6900752BF1 /* RouteFinderTests.swift */; }; 31 | E75366F229AFB9D800A78793 /* MenuCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75366F129AFB9D800A78793 /* MenuCardView.swift */; }; 32 | E76C4E3D29B685C400445799 /* NotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76C4E3729B685C400445799 /* NotificationsManager.swift */; }; 33 | E76C4E3E29B685C400445799 /* LayoutExperienceSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76C4E3929B685C400445799 /* LayoutExperienceSetting.swift */; }; 34 | E76C4E3F29B685C400445799 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76C4E3B29B685C400445799 /* SettingsView.swift */; }; 35 | E76C4E4029B685C400445799 /* LayoutExperienceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76C4E3C29B685C400445799 /* LayoutExperienceView.swift */; }; 36 | E76C4E4D29B68EDA00445799 /* TwoColumnMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76C4E4C29B68EDA00445799 /* TwoColumnMenuView.swift */; }; 37 | E784B5F2298D691B00558357 /* Food.swift in Sources */ = {isa = PBXBuildFile; fileRef = E784B5F1298D691B00558357 /* Food.swift */; }; 38 | E784B5F4298D6E2700558357 /* MenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E784B5F3298D6E2700558357 /* MenuItemView.swift */; }; 39 | E784B5F6298D6EC800558357 /* FoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E784B5F5298D6EC800558357 /* FoodDetailView.swift */; }; 40 | E7AB593D2997AA2A000852B6 /* InvalidProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AB593C2997AA2A000852B6 /* InvalidProductView.swift */; }; 41 | E7C330FD299D15DF00CB6BA7 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = E7C330FC299D15DF00CB6BA7 /* FirebaseMessaging */; }; 42 | E7D69BBA29958DF000F73871 /* PromoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D69BB929958DF000F73871 /* PromoView.swift */; }; 43 | E7E3024029B4AA050012089D /* MenuCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E3023F29B4AA050012089D /* MenuCategory.swift */; }; 44 | E7E3024329B4AE230012089D /* SideBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E3024229B4AE230012089D /* SideBarView.swift */; }; 45 | E7E3024629B4B4260012089D /* SwiftUIPreviews+Devices.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E3024529B4B4260012089D /* SwiftUIPreviews+Devices.swift */; }; 46 | E7E3024829B5F0440012089D /* MenuListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E3024729B5F0440012089D /* MenuListView.swift */; }; 47 | E7E3024A29B601100012089D /* MenuGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E3024929B601100012089D /* MenuGridView.swift */; }; 48 | /* End PBXBuildFile section */ 49 | 50 | /* Begin PBXContainerItemProxy section */ 51 | E70D37D82993A1D9002BEB34 /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = E751599A298D680800CDEC3E /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = E75159A1298D680800CDEC3E; 56 | remoteInfo = "Introduction to NavigationStack"; 57 | }; 58 | /* End PBXContainerItemProxy section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | E702AE652991625E00ABE61B /* CartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartButton.swift; sourceTree = ""; }; 62 | E702AE67299166E100ABE61B /* ShoppingCartManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShoppingCartManager.swift; sourceTree = ""; }; 63 | E702AE692991793600ABE61B /* IngredientsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IngredientsDetailView.swift; sourceTree = ""; }; 64 | E702AE6B29917AAC00ABE61B /* AllergiesDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllergiesDetailView.swift; sourceTree = ""; }; 65 | E70D37CC2992AD00002BEB34 /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; 66 | E70D37CE29939660002BEB34 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 67 | E70D37D42993A1D9002BEB34 /* Introduction to NavigationStackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Introduction to NavigationStackTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | E70D37D62993A1D9002BEB34 /* Introduction_to_NavigationStackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Introduction_to_NavigationStackTests.swift; sourceTree = ""; }; 69 | E70D37DD2993A353002BEB34 /* RouteModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteModelTests.swift; sourceTree = ""; }; 70 | E72C7F8829B7E89C0070B26D /* ThreeColumnMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeColumnMenuView.swift; sourceTree = ""; }; 71 | E72C83B4298E6C1E0020DB0A /* DrinkDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrinkDetailView.swift; sourceTree = ""; }; 72 | E72C83B6298E6F7E0020DB0A /* DessertDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DessertDetailView.swift; sourceTree = ""; }; 73 | E733409E29917C9E00F8B945 /* LocationsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsDetailView.swift; sourceTree = ""; }; 74 | E73340A029917DAC00F8B945 /* LocationMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMapView.swift; sourceTree = ""; }; 75 | E73340A22991852400F8B945 /* CartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartView.swift; sourceTree = ""; }; 76 | E734AA6F29959FEF000B8085 /* RouteFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteFinder.swift; sourceTree = ""; }; 77 | E73F8D7329955B62003EEBC4 /* Introduction-to-NavigationStack-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Introduction-to-NavigationStack-Info.plist"; sourceTree = SOURCE_ROOT; }; 78 | E73F8D742995780B003EEBC4 /* ProductsFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsFetcher.swift; sourceTree = ""; }; 79 | E75159A2298D680800CDEC3E /* Introduction to NavigationStack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Introduction to NavigationStack.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | E75159A5298D680800CDEC3E /* Introduction_to_NavigationStackApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Introduction_to_NavigationStackApp.swift; sourceTree = ""; }; 81 | E75159A7298D680800CDEC3E /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; 82 | E75159A9298D680A00CDEC3E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 83 | E75159AC298D680A00CDEC3E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 84 | E7520BA52997DE6900752BF1 /* RouteFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteFinderTests.swift; sourceTree = ""; }; 85 | E75366F129AFB9D800A78793 /* MenuCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCardView.swift; sourceTree = ""; }; 86 | E76C4E3729B685C400445799 /* NotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = ""; }; 87 | E76C4E3929B685C400445799 /* LayoutExperienceSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutExperienceSetting.swift; sourceTree = ""; }; 88 | E76C4E3B29B685C400445799 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 89 | E76C4E3C29B685C400445799 /* LayoutExperienceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutExperienceView.swift; sourceTree = ""; }; 90 | E76C4E4C29B68EDA00445799 /* TwoColumnMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoColumnMenuView.swift; sourceTree = ""; }; 91 | E784B5F1298D691B00558357 /* Food.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Food.swift; sourceTree = ""; }; 92 | E784B5F3298D6E2700558357 /* MenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemView.swift; sourceTree = ""; }; 93 | E784B5F5298D6EC800558357 /* FoodDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoodDetailView.swift; sourceTree = ""; }; 94 | E7AB593C2997AA2A000852B6 /* InvalidProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidProductView.swift; sourceTree = ""; }; 95 | E7C330FE299E30E400CB6BA7 /* Introduction to NavigationStack.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Introduction to NavigationStack.entitlements"; sourceTree = ""; }; 96 | E7D69BB929958DF000F73871 /* PromoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoView.swift; sourceTree = ""; }; 97 | E7E3023F29B4AA050012089D /* MenuCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCategory.swift; sourceTree = ""; }; 98 | E7E3024229B4AE230012089D /* SideBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarView.swift; sourceTree = ""; }; 99 | E7E3024529B4B4260012089D /* SwiftUIPreviews+Devices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUIPreviews+Devices.swift"; sourceTree = ""; }; 100 | E7E3024729B5F0440012089D /* MenuListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuListView.swift; sourceTree = ""; }; 101 | E7E3024929B601100012089D /* MenuGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuGridView.swift; sourceTree = ""; }; 102 | /* End PBXFileReference section */ 103 | 104 | /* Begin PBXFrameworksBuildPhase section */ 105 | E70D37D12993A1D9002BEB34 /* Frameworks */ = { 106 | isa = PBXFrameworksBuildPhase; 107 | buildActionMask = 2147483647; 108 | files = ( 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | E751599F298D680800CDEC3E /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | E7C330FD299D15DF00CB6BA7 /* FirebaseMessaging in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXFrameworksBuildPhase section */ 121 | 122 | /* Begin PBXGroup section */ 123 | E70D37D52993A1D9002BEB34 /* Introduction to NavigationStackTests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | E70D37D62993A1D9002BEB34 /* Introduction_to_NavigationStackTests.swift */, 127 | E70D37DD2993A353002BEB34 /* RouteModelTests.swift */, 128 | E7520BA52997DE6900752BF1 /* RouteFinderTests.swift */, 129 | ); 130 | path = "Introduction to NavigationStackTests"; 131 | sourceTree = ""; 132 | }; 133 | E73340A42992483A00F8B945 /* Cart */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | E73340A62992484C00F8B945 /* Manager */, 137 | E73340A52992484400F8B945 /* Views */, 138 | ); 139 | path = Cart; 140 | sourceTree = ""; 141 | }; 142 | E73340A52992484400F8B945 /* Views */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | E702AE652991625E00ABE61B /* CartButton.swift */, 146 | E73340A22991852400F8B945 /* CartView.swift */, 147 | ); 148 | path = Views; 149 | sourceTree = ""; 150 | }; 151 | E73340A62992484C00F8B945 /* Manager */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | E702AE67299166E100ABE61B /* ShoppingCartManager.swift */, 155 | ); 156 | path = Manager; 157 | sourceTree = ""; 158 | }; 159 | E73340A72992488100F8B945 /* Menu */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | E73340AA2992489C00F8B945 /* Views */, 163 | E73340A92992489600F8B945 /* Models */, 164 | ); 165 | path = Menu; 166 | sourceTree = ""; 167 | }; 168 | E73340A82992488E00F8B945 /* Food */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | E73340AD299248DE00F8B945 /* Views */, 172 | ); 173 | path = Food; 174 | sourceTree = ""; 175 | }; 176 | E73340A92992489600F8B945 /* Models */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | E784B5F1298D691B00558357 /* Food.swift */, 180 | ); 181 | path = Models; 182 | sourceTree = ""; 183 | }; 184 | E73340AA2992489C00F8B945 /* Views */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | E784B5F3298D6E2700558357 /* MenuItemView.swift */, 188 | E75159A7298D680800CDEC3E /* MenuView.swift */, 189 | E75366F129AFB9D800A78793 /* MenuCardView.swift */, 190 | E7E3024729B5F0440012089D /* MenuListView.swift */, 191 | E7E3024929B601100012089D /* MenuGridView.swift */, 192 | E76C4E4C29B68EDA00445799 /* TwoColumnMenuView.swift */, 193 | E72C7F8829B7E89C0070B26D /* ThreeColumnMenuView.swift */, 194 | ); 195 | path = Views; 196 | sourceTree = ""; 197 | }; 198 | E73340AB299248C700F8B945 /* Drinks */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | E73340AC299248D000F8B945 /* Views */, 202 | ); 203 | path = Drinks; 204 | sourceTree = ""; 205 | }; 206 | E73340AC299248D000F8B945 /* Views */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | E72C83B4298E6C1E0020DB0A /* DrinkDetailView.swift */, 210 | ); 211 | path = Views; 212 | sourceTree = ""; 213 | }; 214 | E73340AD299248DE00F8B945 /* Views */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | E784B5F5298D6EC800558357 /* FoodDetailView.swift */, 218 | ); 219 | path = Views; 220 | sourceTree = ""; 221 | }; 222 | E73340AE299248F700F8B945 /* Locations */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | E73340AF299248FF00F8B945 /* Views */, 226 | ); 227 | path = Locations; 228 | sourceTree = ""; 229 | }; 230 | E73340AF299248FF00F8B945 /* Views */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | E733409E29917C9E00F8B945 /* LocationsDetailView.swift */, 234 | E73340A029917DAC00F8B945 /* LocationMapView.swift */, 235 | ); 236 | path = Views; 237 | sourceTree = ""; 238 | }; 239 | E73340B02992490A00F8B945 /* Desserts */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | E73340B12992491400F8B945 /* Views */, 243 | ); 244 | path = Desserts; 245 | sourceTree = ""; 246 | }; 247 | E73340B12992491400F8B945 /* Views */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | E72C83B6298E6F7E0020DB0A /* DessertDetailView.swift */, 251 | ); 252 | path = Views; 253 | sourceTree = ""; 254 | }; 255 | E73340B22992491F00F8B945 /* Ingredients */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | E73340B32992492A00F8B945 /* Views */, 259 | ); 260 | path = Ingredients; 261 | sourceTree = ""; 262 | }; 263 | E73340B32992492A00F8B945 /* Views */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | E702AE692991793600ABE61B /* IngredientsDetailView.swift */, 267 | ); 268 | path = Views; 269 | sourceTree = ""; 270 | }; 271 | E73340B42992493400F8B945 /* Allergies */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | E73340B52992493D00F8B945 /* Views */, 275 | ); 276 | path = Allergies; 277 | sourceTree = ""; 278 | }; 279 | E73340B52992493D00F8B945 /* Views */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | E702AE6B29917AAC00ABE61B /* AllergiesDetailView.swift */, 283 | ); 284 | path = Views; 285 | sourceTree = ""; 286 | }; 287 | E7515999298D680800CDEC3E = { 288 | isa = PBXGroup; 289 | children = ( 290 | E75159A4298D680800CDEC3E /* Introduction to NavigationStack */, 291 | E70D37D52993A1D9002BEB34 /* Introduction to NavigationStackTests */, 292 | E75159A3298D680800CDEC3E /* Products */, 293 | ); 294 | sourceTree = ""; 295 | }; 296 | E75159A3298D680800CDEC3E /* Products */ = { 297 | isa = PBXGroup; 298 | children = ( 299 | E75159A2298D680800CDEC3E /* Introduction to NavigationStack.app */, 300 | E70D37D42993A1D9002BEB34 /* Introduction to NavigationStackTests.xctest */, 301 | ); 302 | name = Products; 303 | sourceTree = ""; 304 | }; 305 | E75159A4298D680800CDEC3E /* Introduction to NavigationStack */ = { 306 | isa = PBXGroup; 307 | children = ( 308 | E7E3024429B4B4020012089D /* Extensions */, 309 | E7E3023D29B4A9470012089D /* Sidebar */, 310 | E7C330FE299E30E400CB6BA7 /* Introduction to NavigationStack.entitlements */, 311 | E734AA6F29959FEF000B8085 /* RouteFinder.swift */, 312 | E76C4E3629B685C400445799 /* Settings */, 313 | E7D69BB729958DD000F73871 /* Promo */, 314 | E73F8D742995780B003EEBC4 /* ProductsFetcher.swift */, 315 | E73F8D7329955B62003EEBC4 /* Introduction-to-NavigationStack-Info.plist */, 316 | E70D37CE29939660002BEB34 /* Route.swift */, 317 | E70D37CC2992AD00002BEB34 /* NavigationRouter.swift */, 318 | E73340B42992493400F8B945 /* Allergies */, 319 | E73340B22992491F00F8B945 /* Ingredients */, 320 | E73340B02992490A00F8B945 /* Desserts */, 321 | E73340AE299248F700F8B945 /* Locations */, 322 | E73340AB299248C700F8B945 /* Drinks */, 323 | E73340A82992488E00F8B945 /* Food */, 324 | E73340A72992488100F8B945 /* Menu */, 325 | E73340A42992483A00F8B945 /* Cart */, 326 | E75159A5298D680800CDEC3E /* Introduction_to_NavigationStackApp.swift */, 327 | E7AB593C2997AA2A000852B6 /* InvalidProductView.swift */, 328 | E75159A9298D680A00CDEC3E /* Assets.xcassets */, 329 | E75159AB298D680A00CDEC3E /* Preview Content */, 330 | ); 331 | path = "Introduction to NavigationStack"; 332 | sourceTree = ""; 333 | }; 334 | E75159AB298D680A00CDEC3E /* Preview Content */ = { 335 | isa = PBXGroup; 336 | children = ( 337 | E75159AC298D680A00CDEC3E /* Preview Assets.xcassets */, 338 | ); 339 | path = "Preview Content"; 340 | sourceTree = ""; 341 | }; 342 | E76C4E3629B685C400445799 /* Settings */ = { 343 | isa = PBXGroup; 344 | children = ( 345 | E76C4E3729B685C400445799 /* NotificationsManager.swift */, 346 | E76C4E3829B685C400445799 /* Model */, 347 | E76C4E3A29B685C400445799 /* View */, 348 | ); 349 | path = Settings; 350 | sourceTree = ""; 351 | }; 352 | E76C4E3829B685C400445799 /* Model */ = { 353 | isa = PBXGroup; 354 | children = ( 355 | E76C4E3929B685C400445799 /* LayoutExperienceSetting.swift */, 356 | ); 357 | path = Model; 358 | sourceTree = ""; 359 | }; 360 | E76C4E3A29B685C400445799 /* View */ = { 361 | isa = PBXGroup; 362 | children = ( 363 | E76C4E3B29B685C400445799 /* SettingsView.swift */, 364 | E76C4E3C29B685C400445799 /* LayoutExperienceView.swift */, 365 | ); 366 | path = View; 367 | sourceTree = ""; 368 | }; 369 | E7D69BB729958DD000F73871 /* Promo */ = { 370 | isa = PBXGroup; 371 | children = ( 372 | E7D69BB829958DD700F73871 /* Views */, 373 | ); 374 | path = Promo; 375 | sourceTree = ""; 376 | }; 377 | E7D69BB829958DD700F73871 /* Views */ = { 378 | isa = PBXGroup; 379 | children = ( 380 | E7D69BB929958DF000F73871 /* PromoView.swift */, 381 | ); 382 | path = Views; 383 | sourceTree = ""; 384 | }; 385 | E7E3023D29B4A9470012089D /* Sidebar */ = { 386 | isa = PBXGroup; 387 | children = ( 388 | E7E3024129B4AE0E0012089D /* View */, 389 | E7E3023E29B4A9DA0012089D /* Model */, 390 | ); 391 | path = Sidebar; 392 | sourceTree = ""; 393 | }; 394 | E7E3023E29B4A9DA0012089D /* Model */ = { 395 | isa = PBXGroup; 396 | children = ( 397 | E7E3023F29B4AA050012089D /* MenuCategory.swift */, 398 | ); 399 | path = Model; 400 | sourceTree = ""; 401 | }; 402 | E7E3024129B4AE0E0012089D /* View */ = { 403 | isa = PBXGroup; 404 | children = ( 405 | E7E3024229B4AE230012089D /* SideBarView.swift */, 406 | ); 407 | path = View; 408 | sourceTree = ""; 409 | }; 410 | E7E3024429B4B4020012089D /* Extensions */ = { 411 | isa = PBXGroup; 412 | children = ( 413 | E7E3024529B4B4260012089D /* SwiftUIPreviews+Devices.swift */, 414 | ); 415 | path = Extensions; 416 | sourceTree = ""; 417 | }; 418 | /* End PBXGroup section */ 419 | 420 | /* Begin PBXNativeTarget section */ 421 | E70D37D32993A1D9002BEB34 /* Introduction to NavigationStackTests */ = { 422 | isa = PBXNativeTarget; 423 | buildConfigurationList = E70D37DA2993A1D9002BEB34 /* Build configuration list for PBXNativeTarget "Introduction to NavigationStackTests" */; 424 | buildPhases = ( 425 | E70D37D02993A1D9002BEB34 /* Sources */, 426 | E70D37D12993A1D9002BEB34 /* Frameworks */, 427 | E70D37D22993A1D9002BEB34 /* Resources */, 428 | ); 429 | buildRules = ( 430 | ); 431 | dependencies = ( 432 | E70D37D92993A1D9002BEB34 /* PBXTargetDependency */, 433 | ); 434 | name = "Introduction to NavigationStackTests"; 435 | productName = "Introduction to NavigationStackTests"; 436 | productReference = E70D37D42993A1D9002BEB34 /* Introduction to NavigationStackTests.xctest */; 437 | productType = "com.apple.product-type.bundle.unit-test"; 438 | }; 439 | E75159A1298D680800CDEC3E /* Introduction to NavigationStack */ = { 440 | isa = PBXNativeTarget; 441 | buildConfigurationList = E75159B0298D680A00CDEC3E /* Build configuration list for PBXNativeTarget "Introduction to NavigationStack" */; 442 | buildPhases = ( 443 | E751599E298D680800CDEC3E /* Sources */, 444 | E751599F298D680800CDEC3E /* Frameworks */, 445 | E75159A0298D680800CDEC3E /* Resources */, 446 | ); 447 | buildRules = ( 448 | ); 449 | dependencies = ( 450 | ); 451 | name = "Introduction to NavigationStack"; 452 | packageProductDependencies = ( 453 | E7C330FC299D15DF00CB6BA7 /* FirebaseMessaging */, 454 | ); 455 | productName = "Introduction to NavigationStack"; 456 | productReference = E75159A2298D680800CDEC3E /* Introduction to NavigationStack.app */; 457 | productType = "com.apple.product-type.application"; 458 | }; 459 | /* End PBXNativeTarget section */ 460 | 461 | /* Begin PBXProject section */ 462 | E751599A298D680800CDEC3E /* Project object */ = { 463 | isa = PBXProject; 464 | attributes = { 465 | BuildIndependentTargetsInParallel = 1; 466 | LastSwiftUpdateCheck = 1420; 467 | LastUpgradeCheck = 1420; 468 | TargetAttributes = { 469 | E70D37D32993A1D9002BEB34 = { 470 | CreatedOnToolsVersion = 14.2; 471 | TestTargetID = E75159A1298D680800CDEC3E; 472 | }; 473 | E75159A1298D680800CDEC3E = { 474 | CreatedOnToolsVersion = 14.2; 475 | }; 476 | }; 477 | }; 478 | buildConfigurationList = E751599D298D680800CDEC3E /* Build configuration list for PBXProject "Introduction to NavigationStack" */; 479 | compatibilityVersion = "Xcode 14.0"; 480 | developmentRegion = en; 481 | hasScannedForEncodings = 0; 482 | knownRegions = ( 483 | en, 484 | Base, 485 | ); 486 | mainGroup = E7515999298D680800CDEC3E; 487 | packageReferences = ( 488 | E7C330FB299D15DF00CB6BA7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 489 | ); 490 | productRefGroup = E75159A3298D680800CDEC3E /* Products */; 491 | projectDirPath = ""; 492 | projectRoot = ""; 493 | targets = ( 494 | E75159A1298D680800CDEC3E /* Introduction to NavigationStack */, 495 | E70D37D32993A1D9002BEB34 /* Introduction to NavigationStackTests */, 496 | ); 497 | }; 498 | /* End PBXProject section */ 499 | 500 | /* Begin PBXResourcesBuildPhase section */ 501 | E70D37D22993A1D9002BEB34 /* Resources */ = { 502 | isa = PBXResourcesBuildPhase; 503 | buildActionMask = 2147483647; 504 | files = ( 505 | ); 506 | runOnlyForDeploymentPostprocessing = 0; 507 | }; 508 | E75159A0298D680800CDEC3E /* Resources */ = { 509 | isa = PBXResourcesBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | E75159AD298D680A00CDEC3E /* Preview Assets.xcassets in Resources */, 513 | E75159AA298D680A00CDEC3E /* Assets.xcassets in Resources */, 514 | ); 515 | runOnlyForDeploymentPostprocessing = 0; 516 | }; 517 | /* End PBXResourcesBuildPhase section */ 518 | 519 | /* Begin PBXSourcesBuildPhase section */ 520 | E70D37D02993A1D9002BEB34 /* Sources */ = { 521 | isa = PBXSourcesBuildPhase; 522 | buildActionMask = 2147483647; 523 | files = ( 524 | E7520BA62997DE6900752BF1 /* RouteFinderTests.swift in Sources */, 525 | E70D37D72993A1D9002BEB34 /* Introduction_to_NavigationStackTests.swift in Sources */, 526 | E70D37DE2993A353002BEB34 /* RouteModelTests.swift in Sources */, 527 | ); 528 | runOnlyForDeploymentPostprocessing = 0; 529 | }; 530 | E751599E298D680800CDEC3E /* Sources */ = { 531 | isa = PBXSourcesBuildPhase; 532 | buildActionMask = 2147483647; 533 | files = ( 534 | E75366F229AFB9D800A78793 /* MenuCardView.swift in Sources */, 535 | E734AA7029959FEF000B8085 /* RouteFinder.swift in Sources */, 536 | E75159A8298D680800CDEC3E /* MenuView.swift in Sources */, 537 | E7AB593D2997AA2A000852B6 /* InvalidProductView.swift in Sources */, 538 | E702AE662991625E00ABE61B /* CartButton.swift in Sources */, 539 | E76C4E3D29B685C400445799 /* NotificationsManager.swift in Sources */, 540 | E733409F29917C9E00F8B945 /* LocationsDetailView.swift in Sources */, 541 | E75159A6298D680800CDEC3E /* Introduction_to_NavigationStackApp.swift in Sources */, 542 | E76C4E3E29B685C400445799 /* LayoutExperienceSetting.swift in Sources */, 543 | E73F8D752995780B003EEBC4 /* ProductsFetcher.swift in Sources */, 544 | E72C7F8929B7E89C0070B26D /* ThreeColumnMenuView.swift in Sources */, 545 | E7E3024629B4B4260012089D /* SwiftUIPreviews+Devices.swift in Sources */, 546 | E702AE6C29917AAC00ABE61B /* AllergiesDetailView.swift in Sources */, 547 | E7D69BBA29958DF000F73871 /* PromoView.swift in Sources */, 548 | E70D37CF29939660002BEB34 /* Route.swift in Sources */, 549 | E72C83B7298E6F7E0020DB0A /* DessertDetailView.swift in Sources */, 550 | E73340A129917DAC00F8B945 /* LocationMapView.swift in Sources */, 551 | E702AE6A2991793600ABE61B /* IngredientsDetailView.swift in Sources */, 552 | E7E3024029B4AA050012089D /* MenuCategory.swift in Sources */, 553 | E76C4E3F29B685C400445799 /* SettingsView.swift in Sources */, 554 | E7E3024829B5F0440012089D /* MenuListView.swift in Sources */, 555 | E70D37CD2992AD00002BEB34 /* NavigationRouter.swift in Sources */, 556 | E73340A32991852400F8B945 /* CartView.swift in Sources */, 557 | E76C4E4D29B68EDA00445799 /* TwoColumnMenuView.swift in Sources */, 558 | E784B5F2298D691B00558357 /* Food.swift in Sources */, 559 | E784B5F4298D6E2700558357 /* MenuItemView.swift in Sources */, 560 | E76C4E4029B685C400445799 /* LayoutExperienceView.swift in Sources */, 561 | E72C83B5298E6C1E0020DB0A /* DrinkDetailView.swift in Sources */, 562 | E702AE68299166E100ABE61B /* ShoppingCartManager.swift in Sources */, 563 | E7E3024329B4AE230012089D /* SideBarView.swift in Sources */, 564 | E784B5F6298D6EC800558357 /* FoodDetailView.swift in Sources */, 565 | E7E3024A29B601100012089D /* MenuGridView.swift in Sources */, 566 | ); 567 | runOnlyForDeploymentPostprocessing = 0; 568 | }; 569 | /* End PBXSourcesBuildPhase section */ 570 | 571 | /* Begin PBXTargetDependency section */ 572 | E70D37D92993A1D9002BEB34 /* PBXTargetDependency */ = { 573 | isa = PBXTargetDependency; 574 | target = E75159A1298D680800CDEC3E /* Introduction to NavigationStack */; 575 | targetProxy = E70D37D82993A1D9002BEB34 /* PBXContainerItemProxy */; 576 | }; 577 | /* End PBXTargetDependency section */ 578 | 579 | /* Begin XCBuildConfiguration section */ 580 | E70D37DB2993A1D9002BEB34 /* Debug */ = { 581 | isa = XCBuildConfiguration; 582 | buildSettings = { 583 | BUNDLE_LOADER = "$(TEST_HOST)"; 584 | CODE_SIGN_STYLE = Automatic; 585 | CURRENT_PROJECT_VERSION = 1; 586 | DEVELOPMENT_TEAM = HD69KLV277; 587 | GENERATE_INFOPLIST_FILE = YES; 588 | MARKETING_VERSION = 1.0; 589 | PRODUCT_BUNDLE_IDENTIFIER = "com.tundsdev.Introduction-to-NavigationStackTests"; 590 | PRODUCT_NAME = "$(TARGET_NAME)"; 591 | SWIFT_EMIT_LOC_STRINGS = NO; 592 | SWIFT_VERSION = 5.0; 593 | TARGETED_DEVICE_FAMILY = "1,2"; 594 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Introduction to NavigationStack.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Introduction to NavigationStack"; 595 | }; 596 | name = Debug; 597 | }; 598 | E70D37DC2993A1D9002BEB34 /* Release */ = { 599 | isa = XCBuildConfiguration; 600 | buildSettings = { 601 | BUNDLE_LOADER = "$(TEST_HOST)"; 602 | CODE_SIGN_STYLE = Automatic; 603 | CURRENT_PROJECT_VERSION = 1; 604 | DEVELOPMENT_TEAM = HD69KLV277; 605 | GENERATE_INFOPLIST_FILE = YES; 606 | MARKETING_VERSION = 1.0; 607 | PRODUCT_BUNDLE_IDENTIFIER = "com.tundsdev.Introduction-to-NavigationStackTests"; 608 | PRODUCT_NAME = "$(TARGET_NAME)"; 609 | SWIFT_EMIT_LOC_STRINGS = NO; 610 | SWIFT_VERSION = 5.0; 611 | TARGETED_DEVICE_FAMILY = "1,2"; 612 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Introduction to NavigationStack.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Introduction to NavigationStack"; 613 | }; 614 | name = Release; 615 | }; 616 | E75159AE298D680A00CDEC3E /* Debug */ = { 617 | isa = XCBuildConfiguration; 618 | buildSettings = { 619 | ALWAYS_SEARCH_USER_PATHS = NO; 620 | CLANG_ANALYZER_NONNULL = YES; 621 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 622 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 623 | CLANG_ENABLE_MODULES = YES; 624 | CLANG_ENABLE_OBJC_ARC = YES; 625 | CLANG_ENABLE_OBJC_WEAK = YES; 626 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 627 | CLANG_WARN_BOOL_CONVERSION = YES; 628 | CLANG_WARN_COMMA = YES; 629 | CLANG_WARN_CONSTANT_CONVERSION = YES; 630 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 631 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 632 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 633 | CLANG_WARN_EMPTY_BODY = YES; 634 | CLANG_WARN_ENUM_CONVERSION = YES; 635 | CLANG_WARN_INFINITE_RECURSION = YES; 636 | CLANG_WARN_INT_CONVERSION = YES; 637 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 638 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 639 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 640 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 641 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 642 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 643 | CLANG_WARN_STRICT_PROTOTYPES = YES; 644 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 645 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 646 | CLANG_WARN_UNREACHABLE_CODE = YES; 647 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 648 | COPY_PHASE_STRIP = NO; 649 | DEBUG_INFORMATION_FORMAT = dwarf; 650 | ENABLE_STRICT_OBJC_MSGSEND = YES; 651 | ENABLE_TESTABILITY = YES; 652 | GCC_C_LANGUAGE_STANDARD = gnu11; 653 | GCC_DYNAMIC_NO_PIC = NO; 654 | GCC_NO_COMMON_BLOCKS = YES; 655 | GCC_OPTIMIZATION_LEVEL = 0; 656 | GCC_PREPROCESSOR_DEFINITIONS = ( 657 | "DEBUG=1", 658 | "$(inherited)", 659 | ); 660 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 661 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 662 | GCC_WARN_UNDECLARED_SELECTOR = YES; 663 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 664 | GCC_WARN_UNUSED_FUNCTION = YES; 665 | GCC_WARN_UNUSED_VARIABLE = YES; 666 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 667 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 668 | MTL_FAST_MATH = YES; 669 | ONLY_ACTIVE_ARCH = YES; 670 | SDKROOT = iphoneos; 671 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 672 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 673 | }; 674 | name = Debug; 675 | }; 676 | E75159AF298D680A00CDEC3E /* Release */ = { 677 | isa = XCBuildConfiguration; 678 | buildSettings = { 679 | ALWAYS_SEARCH_USER_PATHS = NO; 680 | CLANG_ANALYZER_NONNULL = YES; 681 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 682 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 683 | CLANG_ENABLE_MODULES = YES; 684 | CLANG_ENABLE_OBJC_ARC = YES; 685 | CLANG_ENABLE_OBJC_WEAK = YES; 686 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 687 | CLANG_WARN_BOOL_CONVERSION = YES; 688 | CLANG_WARN_COMMA = YES; 689 | CLANG_WARN_CONSTANT_CONVERSION = YES; 690 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 691 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 692 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 693 | CLANG_WARN_EMPTY_BODY = YES; 694 | CLANG_WARN_ENUM_CONVERSION = YES; 695 | CLANG_WARN_INFINITE_RECURSION = YES; 696 | CLANG_WARN_INT_CONVERSION = YES; 697 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 698 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 699 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 700 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 701 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 702 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 703 | CLANG_WARN_STRICT_PROTOTYPES = YES; 704 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 705 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 706 | CLANG_WARN_UNREACHABLE_CODE = YES; 707 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 708 | COPY_PHASE_STRIP = NO; 709 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 710 | ENABLE_NS_ASSERTIONS = NO; 711 | ENABLE_STRICT_OBJC_MSGSEND = YES; 712 | GCC_C_LANGUAGE_STANDARD = gnu11; 713 | GCC_NO_COMMON_BLOCKS = YES; 714 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 715 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 716 | GCC_WARN_UNDECLARED_SELECTOR = YES; 717 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 718 | GCC_WARN_UNUSED_FUNCTION = YES; 719 | GCC_WARN_UNUSED_VARIABLE = YES; 720 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 721 | MTL_ENABLE_DEBUG_INFO = NO; 722 | MTL_FAST_MATH = YES; 723 | SDKROOT = iphoneos; 724 | SWIFT_COMPILATION_MODE = wholemodule; 725 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 726 | VALIDATE_PRODUCT = YES; 727 | }; 728 | name = Release; 729 | }; 730 | E75159B1298D680A00CDEC3E /* Debug */ = { 731 | isa = XCBuildConfiguration; 732 | buildSettings = { 733 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 734 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 735 | CODE_SIGN_ENTITLEMENTS = "Introduction to NavigationStack/Introduction to NavigationStack.entitlements"; 736 | CODE_SIGN_STYLE = Automatic; 737 | CURRENT_PROJECT_VERSION = 1; 738 | DEVELOPMENT_ASSET_PATHS = "\"Introduction to NavigationStack/Preview Content\""; 739 | DEVELOPMENT_TEAM = N593J3PQL4; 740 | ENABLE_PREVIEWS = YES; 741 | GENERATE_INFOPLIST_FILE = YES; 742 | INFOPLIST_FILE = "Introduction-to-NavigationStack-Info.plist"; 743 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 744 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 745 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 746 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 747 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 748 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 749 | LD_RUNPATH_SEARCH_PATHS = ( 750 | "$(inherited)", 751 | "@executable_path/Frameworks", 752 | ); 753 | MARKETING_VERSION = 1.0; 754 | PRODUCT_BUNDLE_IDENTIFIER = "com.tundsdev.Introduction-to-NavigationStack-test-push"; 755 | PRODUCT_NAME = "$(TARGET_NAME)"; 756 | SWIFT_EMIT_LOC_STRINGS = YES; 757 | SWIFT_VERSION = 5.0; 758 | TARGETED_DEVICE_FAMILY = "1,2"; 759 | }; 760 | name = Debug; 761 | }; 762 | E75159B2298D680A00CDEC3E /* Release */ = { 763 | isa = XCBuildConfiguration; 764 | buildSettings = { 765 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 766 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 767 | CODE_SIGN_ENTITLEMENTS = "Introduction to NavigationStack/Introduction to NavigationStack.entitlements"; 768 | CODE_SIGN_STYLE = Automatic; 769 | CURRENT_PROJECT_VERSION = 1; 770 | DEVELOPMENT_ASSET_PATHS = "\"Introduction to NavigationStack/Preview Content\""; 771 | DEVELOPMENT_TEAM = N593J3PQL4; 772 | ENABLE_PREVIEWS = YES; 773 | GENERATE_INFOPLIST_FILE = YES; 774 | INFOPLIST_FILE = "Introduction-to-NavigationStack-Info.plist"; 775 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 776 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 777 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 778 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 779 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 780 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 781 | LD_RUNPATH_SEARCH_PATHS = ( 782 | "$(inherited)", 783 | "@executable_path/Frameworks", 784 | ); 785 | MARKETING_VERSION = 1.0; 786 | PRODUCT_BUNDLE_IDENTIFIER = "com.tundsdev.Introduction-to-NavigationStack-test-push"; 787 | PRODUCT_NAME = "$(TARGET_NAME)"; 788 | SWIFT_EMIT_LOC_STRINGS = YES; 789 | SWIFT_VERSION = 5.0; 790 | TARGETED_DEVICE_FAMILY = "1,2"; 791 | }; 792 | name = Release; 793 | }; 794 | /* End XCBuildConfiguration section */ 795 | 796 | /* Begin XCConfigurationList section */ 797 | E70D37DA2993A1D9002BEB34 /* Build configuration list for PBXNativeTarget "Introduction to NavigationStackTests" */ = { 798 | isa = XCConfigurationList; 799 | buildConfigurations = ( 800 | E70D37DB2993A1D9002BEB34 /* Debug */, 801 | E70D37DC2993A1D9002BEB34 /* Release */, 802 | ); 803 | defaultConfigurationIsVisible = 0; 804 | defaultConfigurationName = Release; 805 | }; 806 | E751599D298D680800CDEC3E /* Build configuration list for PBXProject "Introduction to NavigationStack" */ = { 807 | isa = XCConfigurationList; 808 | buildConfigurations = ( 809 | E75159AE298D680A00CDEC3E /* Debug */, 810 | E75159AF298D680A00CDEC3E /* Release */, 811 | ); 812 | defaultConfigurationIsVisible = 0; 813 | defaultConfigurationName = Release; 814 | }; 815 | E75159B0298D680A00CDEC3E /* Build configuration list for PBXNativeTarget "Introduction to NavigationStack" */ = { 816 | isa = XCConfigurationList; 817 | buildConfigurations = ( 818 | E75159B1298D680A00CDEC3E /* Debug */, 819 | E75159B2298D680A00CDEC3E /* Release */, 820 | ); 821 | defaultConfigurationIsVisible = 0; 822 | defaultConfigurationName = Release; 823 | }; 824 | /* End XCConfigurationList section */ 825 | 826 | /* Begin XCRemoteSwiftPackageReference section */ 827 | E7C330FB299D15DF00CB6BA7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { 828 | isa = XCRemoteSwiftPackageReference; 829 | repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; 830 | requirement = { 831 | branch = master; 832 | kind = branch; 833 | }; 834 | }; 835 | /* End XCRemoteSwiftPackageReference section */ 836 | 837 | /* Begin XCSwiftPackageProductDependency section */ 838 | E7C330FC299D15DF00CB6BA7 /* FirebaseMessaging */ = { 839 | isa = XCSwiftPackageProductDependency; 840 | package = E7C330FB299D15DF00CB6BA7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 841 | productName = FirebaseMessaging; 842 | }; 843 | /* End XCSwiftPackageProductDependency section */ 844 | }; 845 | rootObject = E751599A298D680800CDEC3E /* Project object */; 846 | } 847 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "abseil-cpp-swiftpm", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", 7 | "state" : { 8 | "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", 9 | "version" : "0.20220203.2" 10 | } 11 | }, 12 | { 13 | "identity" : "boringssl-swiftpm", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/firebase/boringssl-SwiftPM.git", 16 | "state" : { 17 | "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", 18 | "version" : "0.9.1" 19 | } 20 | }, 21 | { 22 | "identity" : "firebase-ios-sdk", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/firebase/firebase-ios-sdk", 25 | "state" : { 26 | "branch" : "master", 27 | "revision" : "52f246ce87614f4ba4569d112972928995554204" 28 | } 29 | }, 30 | { 31 | "identity" : "googleappmeasurement", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/google/GoogleAppMeasurement.git", 34 | "state" : { 35 | "revision" : "9a09ece724128e8d1e14c5133b87c0e236844ac0", 36 | "version" : "10.4.0" 37 | } 38 | }, 39 | { 40 | "identity" : "googledatatransport", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/google/GoogleDataTransport.git", 43 | "state" : { 44 | "revision" : "f6b558e3f801f2cac336b04f615ce111fa9ddaa0", 45 | "version" : "9.2.1" 46 | } 47 | }, 48 | { 49 | "identity" : "googleutilities", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/google/GoogleUtilities.git", 52 | "state" : { 53 | "revision" : "0543562f85620b5b7c510c6bcbef75b562a5127b", 54 | "version" : "7.11.0" 55 | } 56 | }, 57 | { 58 | "identity" : "grpc-ios", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/grpc/grpc-ios.git", 61 | "state" : { 62 | "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", 63 | "version" : "1.44.3-grpc" 64 | } 65 | }, 66 | { 67 | "identity" : "gtm-session-fetcher", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/google/gtm-session-fetcher.git", 70 | "state" : { 71 | "revision" : "96d7cc73a71ce950723aa3c50ce4fb275ae180b8", 72 | "version" : "3.1.0" 73 | } 74 | }, 75 | { 76 | "identity" : "leveldb", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/firebase/leveldb.git", 79 | "state" : { 80 | "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", 81 | "version" : "1.22.2" 82 | } 83 | }, 84 | { 85 | "identity" : "nanopb", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/firebase/nanopb.git", 88 | "state" : { 89 | "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", 90 | "version" : "2.30909.0" 91 | } 92 | }, 93 | { 94 | "identity" : "promises", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/google/promises.git", 97 | "state" : { 98 | "revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a", 99 | "version" : "2.2.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-protobuf", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-protobuf.git", 106 | "state" : { 107 | "revision" : "ab3a58b7209a17d781c0d1dbb3e1ff3da306bae8", 108 | "version" : "1.20.3" 109 | } 110 | } 111 | ], 112 | "version" : 2 113 | } 114 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/project.xcworkspace/xcuserdata/tunde.adegoroye.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunds/SwiftUI-Navigation-Multiplatform-Example/240160eebf0b272feb39c77673dd864a49f03302/Project/Introduction to NavigationStack.xcodeproj/project.xcworkspace/xcuserdata/tunde.adegoroye.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/xcshareddata/xcschemes/Introduction to NavigationStack.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/xcuserdata/tunde.adegoroye.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 17 | 18 | 19 | 21 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack.xcodeproj/xcuserdata/tunde.adegoroye.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Introduction to NavigationStack.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Promises (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | Promises (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | Promises (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | SuppressBuildableAutocreation 35 | 36 | E70D37D32993A1D9002BEB34 37 | 38 | primary 39 | 40 | 41 | E75159A1298D680800CDEC3E 42 | 43 | primary 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Allergies/Views/AllergiesDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllergiesDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AllergiesDetailView: View { 11 | 12 | let allergies: [Allergie] 13 | 14 | var body: some View { 15 | List { 16 | 17 | Section { 18 | ForEach(allergies, id: \.name) { allergy in 19 | /*@START_MENU_TOKEN@*/Text(allergy.name)/*@END_MENU_TOKEN@*/ 20 | } 21 | } footer: { 22 | Text("This item may contain traces of the following above") 23 | } 24 | } 25 | .navigationTitle("Allergies") 26 | } 27 | } 28 | 29 | struct AllergiesDetailView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | NavigationStack { 32 | AllergiesDetailView(allergies: desserts[0].allergies!) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/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 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Logo.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Assets.xcassets/AppIcon.appiconset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunds/SwiftUI-Navigation-Multiplatform-Example/240160eebf0b272feb39c77673dd864a49f03302/Project/Introduction to NavigationStack/Assets.xcassets/AppIcon.appiconset/Logo.png -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Cart/Manager/ShoppingCartManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShoppingCartManager.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CartItem: Identifiable, Hashable { 11 | let id: String 12 | let title: String 13 | let name: String 14 | let price: Decimal 15 | 16 | init(_ item: any MenuItem) { 17 | self.id = "\(item.name)_\(item.title)" 18 | self.title = item.title 19 | self.name = item.name 20 | self.price = item.price 21 | } 22 | } 23 | 24 | final class ShoppingCartManager: ObservableObject { 25 | 26 | @Published private(set) var items: [CartItem] = [] 27 | @Published private(set) var promo: PromoData? 28 | 29 | func add(_ item: any MenuItem) { 30 | items.append(CartItem(item)) 31 | } 32 | 33 | func remove(id: String) { 34 | items.removeAll(where: { $0.id == id }) 35 | } 36 | 37 | func getGroupedCart() -> [CartItem: Int] { 38 | var itemCounts = [CartItem: Int]() 39 | for item in items { 40 | itemCounts[item, default: 0] += 1 41 | } 42 | return itemCounts 43 | } 44 | 45 | func getTotal() -> Decimal { 46 | let total = items.reduce(0) { $0 + $1.price } 47 | guard let discount = promo?.pct else { return total } 48 | return total - (discount * total) 49 | } 50 | 51 | func set(promo: PromoData) { 52 | self.promo = promo 53 | } 54 | 55 | func removePromo() { 56 | self.promo = nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Cart/Views/CartButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartButton.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CartButton: View { 11 | 12 | let count: Int 13 | let didTap: () -> () 14 | 15 | var body: some View { 16 | Button { 17 | didTap() 18 | } label: { 19 | Image(systemName: "cart") 20 | .symbolVariant(.fill) 21 | .padding(4) 22 | } 23 | .overlay(alignment: .topTrailing) { 24 | if count > 0 { 25 | badge 26 | } 27 | } 28 | } 29 | } 30 | 31 | private extension CartButton { 32 | 33 | var badge: some View { 34 | Text("\(count)") 35 | .foregroundColor(.white) 36 | .padding(6) 37 | .font(.caption2.bold()) 38 | .monospacedDigit() 39 | .background( 40 | Circle() 41 | .fill(.red) 42 | ) 43 | .offset(x: 2, y: -2) 44 | } 45 | } 46 | 47 | struct CartButton_Previews: PreviewProvider { 48 | static var previews: some View { 49 | CartButton(count: 0) { 50 | 51 | } 52 | .previewLayout(.sizeThatFits) 53 | .padding() 54 | 55 | CartButton(count: 1) { 56 | 57 | } 58 | .previewLayout(.sizeThatFits) 59 | .padding() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Cart/Views/CartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CartView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | 15 | var body: some View { 16 | List { 17 | let items = cartManager.getGroupedCart() 18 | ForEach(items.keys.sorted(by: { $0.title < $1.title })) { key in 19 | let count = items[key]! 20 | LabeledContent { 21 | let price = key.price * Decimal(count) 22 | Text(price, 23 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 24 | } label: { 25 | let multiplierVw = Text("x\(count)").font(.footnote).bold() 26 | Text("\(multiplierVw) \(key.name) \(key.title)") 27 | } 28 | .swipeActions { 29 | Button(role: .destructive) { 30 | cartManager.remove(id: key.id) 31 | } label: { 32 | Image(systemName: "trash") 33 | } 34 | } 35 | } 36 | 37 | if let promo = cartManager.promo { 38 | 39 | Section { 40 | 41 | LabeledContent { 42 | if let pct = promo.pct { 43 | Text(pct, format: .percent) 44 | } else { 45 | Text("N/A") 46 | } 47 | } label: { 48 | Text(promo.desc) 49 | } 50 | .swipeActions { 51 | Button(role: .destructive) { 52 | cartManager.removePromo() 53 | } label: { 54 | Image(systemName: "trash") 55 | } 56 | } 57 | } 58 | } 59 | 60 | Section { 61 | LabeledContent { 62 | Text(cartManager.getTotal(), 63 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 64 | .bold() 65 | } label: { 66 | Text("Total") 67 | } 68 | } 69 | } 70 | .navigationTitle("Basket") 71 | } 72 | } 73 | 74 | struct CartView_Previews: PreviewProvider { 75 | 76 | static var previews: some View { 77 | let manager = ShoppingCartManager() 78 | NavigationStack { 79 | CartView() 80 | .environmentObject(manager) 81 | .onAppear { 82 | manager.set(promo: .init(desc: "Great promo", 83 | pct: 0.5)) 84 | for food in foods { 85 | manager.add(food) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Desserts/Views/DessertDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DessertDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 04/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DessertDetailView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | 15 | let dessert: Dessert 16 | 17 | var body: some View { 18 | List { 19 | 20 | Section { 21 | LabeledContent("Icon", value: dessert.name) 22 | LabeledContent("Name", value: dessert.title) 23 | LabeledContent { 24 | Text(dessert.price, 25 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 26 | } label: { 27 | Text("Price") 28 | } 29 | LabeledContent("Cold?", value: dessert.isCold ? "✅" : "❌") 30 | } 31 | 32 | Section("Description") { 33 | Text(dessert.description) 34 | } 35 | 36 | Section { 37 | Button { 38 | cartManager.add(dessert) 39 | routeManager.reset() 40 | } label: { 41 | Label("Add to cart", systemImage: "cart") 42 | .symbolVariant(.fill) 43 | } 44 | } 45 | 46 | } 47 | .listStyle(.insetGrouped) 48 | .navigationTitle(dessert.title) 49 | .toolbar { 50 | if let productShareDeepLink = Route.buildDeepLink(from: .menuItem(item: dessert)) { 51 | ToolbarItem(placement: .primaryAction) { 52 | ShareLink(item: productShareDeepLink) { 53 | Image(systemName: "square.and.arrow.up") 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | struct DessertDetailView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | NavigationStack { 64 | 65 | DessertDetailView(dessert: desserts[0]) 66 | .environmentObject(ShoppingCartManager()) 67 | .environmentObject(NavigationRouter()) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Drinks/Views/DrinkDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrinkDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 04/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DrinkDetailView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | 15 | let drink: Drink 16 | 17 | var body: some View { 18 | List { 19 | 20 | Section { 21 | LabeledContent("Icon", value: drink.name) 22 | LabeledContent("Name", value: drink.title) 23 | LabeledContent { 24 | Text(drink.price, 25 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 26 | } label: { 27 | Text("Price") 28 | } 29 | LabeledContent("Fizzy?", value: drink.isFizzy ? "✅" : "❌") 30 | } 31 | 32 | Section("Description") { 33 | Text(drink.description) 34 | } 35 | 36 | Section { 37 | Button { 38 | cartManager.add(drink) 39 | routeManager.reset() 40 | } label: { 41 | Label("Add to cart", systemImage: "cart") 42 | .symbolVariant(.fill) 43 | } 44 | } 45 | 46 | } 47 | .listStyle(.insetGrouped) 48 | .navigationTitle(drink.title) 49 | .toolbar { 50 | if let productShareDeepLink = Route.buildDeepLink(from: .menuItem(item: drink)) { 51 | ToolbarItem(placement: .primaryAction) { 52 | ShareLink(item: productShareDeepLink) { 53 | Image(systemName: "square.and.arrow.up") 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | struct DrinkDetailView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | NavigationStack { 64 | DrinkDetailView(drink: drinks[0]) 65 | .environmentObject(ShoppingCartManager()) 66 | .environmentObject(NavigationRouter()) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Extensions/SwiftUIPreviews+Devices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIPreviews+Devices.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 05/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DevicePreviewModifier: ViewModifier { 11 | 12 | enum Device { 13 | case iPhone 14 | case iPad 15 | } 16 | 17 | let device: Device 18 | 19 | func body(content: Content) -> some View { 20 | switch device { 21 | case .iPad: 22 | content 23 | .previewDevice("iPad (10th generation)") 24 | .previewDisplayName("iPad Preview") 25 | .previewInterfaceOrientation(.landscapeLeft) 26 | case .iPhone: 27 | content 28 | .previewDevice("iPhone 14") 29 | .previewDisplayName("iPhone Preview") 30 | } 31 | } 32 | } 33 | 34 | extension View { 35 | 36 | func preview(for device: DevicePreviewModifier.Device) -> some View { 37 | self.modifier(DevicePreviewModifier(device: device)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Food/Views/FoodDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoodDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 03/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FoodDetailView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | let food: Food 15 | 16 | var body: some View { 17 | List { 18 | 19 | Section { 20 | LabeledContent("Icon", value: food.name) 21 | LabeledContent("Name", value: food.title) 22 | LabeledContent { 23 | Text(food.price, 24 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 25 | } label: { 26 | Text("Price") 27 | } 28 | } 29 | 30 | Section("Description") { 31 | Text(food.description) 32 | } 33 | 34 | if food.allergies?.isEmpty == false || 35 | food.ingredients?.isEmpty == false { 36 | 37 | Section("Dietry") { 38 | 39 | if let ingredients = food.ingredients { 40 | NavigationLink(value: Route.ingredients(items: ingredients)) { 41 | let countVw = Text("x\(ingredients.count)").font(.footnote).bold() 42 | Text("\(countVw) Ingredients") 43 | } 44 | } 45 | 46 | if let allergies = food.allergies { 47 | NavigationLink(value: Route.allergies(items: allergies)) { 48 | let countVw = Text("x\(allergies.count)").font(.footnote).bold() 49 | Text("\(countVw) Allergies") 50 | } 51 | } 52 | } 53 | } 54 | 55 | if let places = food.locations { 56 | 57 | Section("Locations") { 58 | 59 | Button("See all locations") { 60 | routeManager.push(to: .locations(places: places)) 61 | } 62 | } 63 | } 64 | 65 | Section { 66 | Button { 67 | cartManager.add(food) 68 | routeManager.reset() 69 | } label: { 70 | Label("Add to cart", systemImage: "cart") 71 | .symbolVariant(.fill) 72 | } 73 | } 74 | 75 | } 76 | .listStyle(.insetGrouped) 77 | .navigationTitle(food.title) 78 | .toolbar { 79 | if let productShareDeepLink = Route.buildDeepLink(from: .menuItem(item: food)) { 80 | ToolbarItem(placement: .primaryAction) { 81 | ShareLink(item: productShareDeepLink) { 82 | Image(systemName: "square.and.arrow.up") 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | struct FoodDetailView_Previews: PreviewProvider { 91 | static var previews: some View { 92 | NavigationStack { 93 | FoodDetailView(food: foods[0]) 94 | .environmentObject(ShoppingCartManager()) 95 | .environmentObject(NavigationRouter()) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Ingredients/Views/IngredientsDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IngredientsDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct IngredientsDetailView: View { 11 | 12 | let ingredients: [Ingredient] 13 | 14 | var body: some View { 15 | List(ingredients, id: \.name) { ingredient in 16 | LabeledContent { 17 | Text(ingredient.quantity / 100, format: .percent) 18 | } label: { 19 | Text(ingredient.name) 20 | } 21 | } 22 | .navigationTitle("Ingredients") 23 | } 24 | } 25 | 26 | struct IngredientsDetailView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | NavigationStack { 29 | IngredientsDetailView(ingredients: foods[0].ingredients!) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Introduction to NavigationStack.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Introduction_to_NavigationStackApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Introduction_to_NavigationStackApp.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 03/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | import UserNotifications 10 | import FirebaseCore 11 | import FirebaseMessaging 12 | 13 | class MyAppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate, MessagingDelegate { 14 | 15 | var app: Introduction_to_NavigationStackApp? 16 | 17 | @objc 18 | func toggleColumnVisibility() { 19 | 20 | 21 | } 22 | 23 | func application(_ application: UIApplication, 24 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 25 | 26 | FirebaseApp.configure() 27 | 28 | FirebaseConfiguration.shared.setLoggerLevel(.debug) 29 | 30 | Messaging.messaging().delegate = self 31 | 32 | UNUserNotificationCenter.current().delegate = self 33 | 34 | application.registerForRemoteNotifications() 35 | 36 | return true 37 | } 38 | 39 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { 40 | if let deepLink = response.notification.request.content.userInfo["link"] as? String, 41 | let url = URL(string: deepLink) { 42 | Task { 43 | await app?.handleDeeplinking(from: url) 44 | } 45 | print("✅ found deep link \(deepLink)") 46 | } 47 | } 48 | 49 | func userNotificationCenter(_ center: UNUserNotificationCenter, 50 | willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { 51 | return [.sound, .badge, .banner, .list] 52 | } 53 | 54 | func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { 55 | 56 | #if DEBUG 57 | print("🚨 FCM Token: \(fcmToken)") 58 | #endif 59 | } 60 | 61 | func application(_ application: UIApplication, 62 | didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 63 | Messaging.messaging().apnsToken = deviceToken 64 | } 65 | } 66 | 67 | @main 68 | struct Introduction_to_NavigationStackApp: App { 69 | 70 | @StateObject private var routerManager = NavigationRouter() 71 | @StateObject private var cartManager = ShoppingCartManager() 72 | @StateObject private var fetcher = ProductsFetcher() 73 | 74 | @UIApplicationDelegateAdaptor private var appDelegate: MyAppDelegate 75 | 76 | @AppStorage("layoutExperience") 77 | var layoutExperience: LayoutExperienceSetting? 78 | 79 | var body: some Scene { 80 | WindowGroup { 81 | TabView { 82 | Group { 83 | MenuView(layoutExperience: $layoutExperience) 84 | .tabItem { 85 | Label("Menu", systemImage: "menucard") 86 | } 87 | .environmentObject(cartManager) 88 | .environmentObject(routerManager) 89 | .environmentObject(fetcher) 90 | .onOpenURL { url in 91 | Task { 92 | await handleDeeplinking(from: url) 93 | } 94 | } 95 | 96 | SettingsView(layoutExperience: $layoutExperience) 97 | .tabItem { 98 | Label("Settings", systemImage: "gear") 99 | } 100 | } 101 | .toolbar(UIDevice.current.userInterfaceIdiom != .phone ? .hidden : .visible, for: .tabBar) 102 | } 103 | .onAppear { 104 | appDelegate.app = self 105 | } 106 | } 107 | } 108 | } 109 | 110 | private extension Introduction_to_NavigationStackApp { 111 | 112 | func handleDeeplinking(from url: URL) async { 113 | 114 | let routeFinder = RouteFinder() 115 | if let route = await routeFinder.find(from: url, 116 | productsFetcher: fetcher) { 117 | switch route { 118 | case .menuItem(let item, _): 119 | routerManager.selectedCategory = fetcher.getCategory(for: item) 120 | routerManager.selectedItemId = item.id 121 | if layoutExperience != .threeColumn { 122 | routerManager.push(to: route) 123 | } 124 | default: 125 | routerManager.selectedCategory = MenuCategory.allCases.first 126 | routerManager.push(to: route) 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/InvalidProductView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvalidProductView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 11/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InvalidProductView: View { 11 | var body: some View { 12 | VStack { 13 | Text("😱") 14 | .font(.system(size: 100)) 15 | Text("Invalid Product") 16 | .font(.largeTitle) 17 | .bold() 18 | Text("Looks like that item doesn't exist anymore") 19 | } 20 | } 21 | } 22 | 23 | struct InvalidProductView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | InvalidProductView() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Locations/Views/LocationMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationMapView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct LocationMapView: View { 12 | 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | let location: Location 15 | 16 | var body: some View { 17 | Map(coordinateRegion: .constant(.init(center: .init(latitude: location.lat, 18 | longitude: location.long), 19 | latitudinalMeters: 100, 20 | longitudinalMeters: 100)), 21 | annotationItems: [location]) { item in 22 | MapMarker(coordinate: .init(latitude: location.lat, 23 | longitude: location.long)) 24 | } 25 | .frame(maxWidth: .infinity, maxHeight: .infinity) 26 | .edgesIgnoringSafeArea(.all) 27 | .toolbar(.hidden, for: .tabBar) 28 | .toolbar(.hidden, for: .navigationBar) 29 | .overlay(alignment: .bottom) { 30 | HStack { 31 | 32 | Group { 33 | Button("Back to Menu") { 34 | routeManager.reset() 35 | } 36 | 37 | Button("Back to Locations") { 38 | routeManager.goBack() 39 | } 40 | 41 | } 42 | .buttonStyle(.borderedProminent) 43 | .controlSize(.large) 44 | 45 | } 46 | } 47 | } 48 | } 49 | 50 | struct LocationMapView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | NavigationStack { 53 | LocationMapView(location: foods[2].locations![0]) 54 | .environmentObject(NavigationRouter()) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Locations/Views/LocationsDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationsDetailView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LocationsDetailView: View { 11 | 12 | let locations: [Location] 13 | 14 | var body: some View { 15 | List { 16 | ForEach(locations, id: \.name) { location in 17 | NavigationLink(location.name, value: Route.map(location: location)) 18 | } 19 | } 20 | .navigationTitle("Locations") 21 | } 22 | } 23 | 24 | struct LocationsDetailView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | NavigationStack { 27 | LocationsDetailView(locations: foods[0].locations!) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Models/Food.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Food.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 03/02/2023. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | protocol MenuItem: Identifiable, Hashable { 12 | var id: String { get } 13 | var name: String { get } 14 | var title: String { get } 15 | var description: String { get } 16 | var price: Decimal { get } 17 | var ingredients: [Ingredient]? { get } 18 | var allergies: [Allergie]? { get } 19 | var locations: [Location]? { get } 20 | } 21 | 22 | struct Food: MenuItem { 23 | var id: String { "\(name)_\(title)" } 24 | let name: String 25 | let title: String 26 | let description: String 27 | let price: Decimal 28 | let ingredients: [Ingredient]? 29 | let allergies: [Allergie]? 30 | let locations: [Location]? 31 | } 32 | 33 | let foods: [Food] = [ 34 | Food(name: "🌯", 35 | title: "Burrito", 36 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 37 | price: 7.99, 38 | ingredients: [ 39 | Ingredient(name: "Ingredient 1", quantity: 10), 40 | Ingredient(name: "Ingredient 2", quantity: 5), 41 | Ingredient(name: "Ingredient 3", quantity: 5) 42 | ], 43 | allergies: [ 44 | Allergie(name: "Allergie 1"), 45 | Allergie(name: "Allergie 2"), 46 | Allergie(name: "Allergie 3") 47 | ], 48 | locations: [ 49 | Location(name: "Location 1", 50 | long: -0.1275, lat: 51.507222), 51 | Location(name: "Location 2", 52 | long: -0.1275, lat: 51.507222), 53 | Location(name: "Location 3", 54 | long: -0.1275, lat: 51.507222), 55 | Location(name: "Location 4", 56 | long: -0.1275, lat: 51.507222) 57 | ]), 58 | Food(name: "🍜", 59 | title: "Ramen", 60 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 61 | price: 10.99, 62 | ingredients: [ 63 | Ingredient(name: "Ingredient 2", quantity: 20), 64 | Ingredient(name: "Ingredient 3", quantity: 11), 65 | Ingredient(name: "Ingredient 4", quantity: 15) 66 | ], 67 | allergies: nil, 68 | locations: [ 69 | Location(name: "Location 1", 70 | long: -0.1275, lat: 51.507222), 71 | Location(name: "Location 2", 72 | long: -0.1275, lat: 51.507222), 73 | Location(name: "Location 3", 74 | long: -0.1275, lat: 51.507222) 75 | ]), 76 | Food(name: "🍔", 77 | title: "Burger", 78 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 79 | price: 4.99, 80 | ingredients: [ 81 | Ingredient(name: "Ingredient 1", quantity: 10), 82 | Ingredient(name: "Ingredient 2", quantity: 5), 83 | Ingredient(name: "Ingredient 3", quantity: 5) 84 | ], 85 | allergies: nil, 86 | locations: [ 87 | Location(name: "Location 1", 88 | long: -0.1275, lat: 51.507222), 89 | Location(name: "Location 2", 90 | long: -0.1275, lat: 51.507222), 91 | Location(name: "Location 3", 92 | long: -0.1275, lat: 51.507222), 93 | Location(name: "Location 4", 94 | long: -0.1275, lat: 51.507222) 95 | ]), 96 | Food(name: "🍕", 97 | title: "Pizza", 98 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 99 | price: 1.99, 100 | ingredients: [ 101 | Ingredient(name: "Ingredient 1", quantity: 10), 102 | Ingredient(name: "Ingredient 2", quantity: 5), 103 | Ingredient(name: "Ingredient 3", quantity: 5) 104 | ], 105 | allergies: [ 106 | Allergie(name: "Allergie 1"), 107 | Allergie(name: "Allergie 2"), 108 | Allergie(name: "Allergie 3") 109 | ], 110 | locations: [ 111 | Location(name: "Location 1", 112 | long: -0.1275, lat: 51.507222), 113 | Location(name: "Location 2", 114 | long: -0.1275, lat: 51.507222), 115 | Location(name: "Location 3", 116 | long: -0.1275, lat: 51.507222), 117 | Location(name: "Location 4", 118 | long: -0.1275, lat: 51.507222) 119 | ]), 120 | Food(name: "🌭", 121 | title: "Hotdog", 122 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 123 | price: 0.99, 124 | ingredients: [ 125 | Ingredient(name: "Ingredient 1", quantity: 10), 126 | Ingredient(name: "Ingredient 2", quantity: 5), 127 | Ingredient(name: "Ingredient 3", quantity: 5) 128 | ], 129 | allergies: nil, 130 | locations: [ 131 | Location(name: "Location 1", 132 | long: -0.1275, lat: 51.507222), 133 | Location(name: "Location 2", 134 | long: -0.1275, lat: 51.507222), 135 | Location(name: "Location 3", 136 | long: -0.1275, lat: 51.507222), 137 | Location(name: "Location 4", 138 | long: -0.1275, lat: 51.507222) 139 | ]), 140 | Food(name: "🧆", 141 | title: "Falafel", 142 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 143 | price: 2.99, 144 | ingredients: [ 145 | Ingredient(name: "Ingredient 1", quantity: 10), 146 | Ingredient(name: "Ingredient 2", quantity: 5), 147 | Ingredient(name: "Ingredient 3", quantity: 5) 148 | ], 149 | allergies: [ 150 | Allergie(name: "Allergie 1"), 151 | Allergie(name: "Allergie 2"), 152 | Allergie(name: "Allergie 3") 153 | ], 154 | locations: [ 155 | Location(name: "Location 1", 156 | long: -0.1275, lat: 51.507222), 157 | Location(name: "Location 2", 158 | long: -0.1275, lat: 51.507222), 159 | Location(name: "Location 3", 160 | long: -0.1275, lat: 51.507222), 161 | Location(name: "Location 4", 162 | long: -0.1275, lat: 51.507222) 163 | ]), 164 | Food(name: "🍝", 165 | title: "Spag Bol", 166 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 167 | price: 12.99, 168 | ingredients: [ 169 | Ingredient(name: "Ingredient 1", quantity: 10), 170 | Ingredient(name: "Ingredient 2", quantity: 5), 171 | Ingredient(name: "Ingredient 3", quantity: 5) 172 | ], 173 | allergies: nil, 174 | locations: [ 175 | Location(name: "Location 1", 176 | long: -0.1275, lat: 51.507222), 177 | Location(name: "Location 2", 178 | long: -0.1275, lat: 51.507222), 179 | Location(name: "Location 3", 180 | long: -0.1275, lat: 51.507222), 181 | Location(name: "Location 4", 182 | long: -0.1275, lat: 51.507222) 183 | ]) 184 | ] 185 | 186 | struct Drink: MenuItem { 187 | var id: String { "\(name)_\(title)" } 188 | let name: String 189 | let title: String 190 | let description: String 191 | let isFizzy: Bool 192 | let price: Decimal 193 | let ingredients: [Ingredient]? 194 | let allergies: [Allergie]? 195 | let locations: [Location]? 196 | } 197 | 198 | let drinks: [Drink] = [ 199 | Drink(name: "🥤", 200 | title: "Soda", 201 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 202 | isFizzy: true, 203 | price: 2.99, 204 | ingredients: [ 205 | Ingredient(name: "Ingredient 1", quantity: 10), 206 | Ingredient(name: "Ingredient 2", quantity: 5), 207 | Ingredient(name: "Ingredient 3", quantity: 5) 208 | ], 209 | allergies: nil, 210 | locations: [ 211 | Location(name: "Location 1", 212 | long: -0.1275, lat: 51.507222), 213 | Location(name: "Location 2", 214 | long: -0.1275, lat: 51.507222), 215 | Location(name: "Location 3", 216 | long: -0.1275, lat: 51.507222), 217 | Location(name: "Location 4", 218 | long: -0.1275, lat: 51.507222) 219 | ]), 220 | Drink(name: "🧋", 221 | title: "Boba Tea", 222 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 223 | isFizzy: false, 224 | price: 3.99, 225 | ingredients: [ 226 | Ingredient(name: "Ingredient 1", quantity: 10), 227 | Ingredient(name: "Ingredient 2", quantity: 5), 228 | Ingredient(name: "Ingredient 3", quantity: 5) 229 | ], 230 | allergies: [ 231 | Allergie(name: "Allergie 1"), 232 | Allergie(name: "Allergie 2"), 233 | Allergie(name: "Allergie 3") 234 | ], 235 | locations: [ 236 | Location(name: "Location 1", 237 | long: -0.1275, lat: 51.507222), 238 | Location(name: "Location 2", 239 | long: -0.1275, lat: 51.507222), 240 | Location(name: "Location 3", 241 | long: -0.1275, lat: 51.507222), 242 | Location(name: "Location 4", 243 | long: -0.1275, lat: 51.507222) 244 | ]), 245 | Drink(name: "🧃", 246 | title: "Juice", 247 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 248 | isFizzy: false, 249 | price: 0.99, 250 | ingredients: [ 251 | Ingredient(name: "Ingredient 1", quantity: 10), 252 | Ingredient(name: "Ingredient 2", quantity: 5), 253 | Ingredient(name: "Ingredient 3", quantity: 5) 254 | ], 255 | allergies: nil, 256 | locations: [ 257 | Location(name: "Location 1", 258 | long: -0.1275, lat: 51.507222), 259 | Location(name: "Location 2", 260 | long: -0.1275, lat: 51.507222), 261 | Location(name: "Location 3", 262 | long: -0.1275, lat: 51.507222), 263 | Location(name: "Location 4", 264 | long: -0.1275, lat: 51.507222) 265 | ]) 266 | ] 267 | 268 | struct Dessert: MenuItem { 269 | var id: String { "\(name)_\(title)" } 270 | let name: String 271 | let title: String 272 | let description: String 273 | let isCold: Bool 274 | let price: Decimal 275 | let ingredients: [Ingredient]? 276 | let allergies: [Allergie]? 277 | let locations: [Location]? 278 | } 279 | 280 | let desserts: [Dessert] = [ 281 | 282 | Dessert(name: "🍦", 283 | title: "Ice Cream", 284 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 285 | isCold: true, 286 | price: 0.99, 287 | ingredients: [ 288 | Ingredient(name: "Ingredient 1", quantity: 10), 289 | Ingredient(name: "Ingredient 2", quantity: 5), 290 | Ingredient(name: "Ingredient 3", quantity: 5) 291 | ], 292 | allergies: [ 293 | Allergie(name: "Allergie 1"), 294 | Allergie(name: "Allergie 2"), 295 | Allergie(name: "Allergie 3") 296 | ], 297 | locations: [ 298 | Location(name: "Location 1", 299 | long: -0.1275, lat: 51.507222), 300 | Location(name: "Location 2", 301 | long: -0.1275, lat: 51.507222), 302 | Location(name: "Location 3", 303 | long: -0.1275, lat: 51.507222), 304 | Location(name: "Location 4", 305 | long: -0.1275, lat: 51.507222) 306 | ]), 307 | Dessert(name: "🍩", 308 | title: "Doughnut", 309 | description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 310 | isCold: false, 311 | price: 0.99, 312 | ingredients: [ 313 | Ingredient(name: "Ingredient 1", quantity: 10), 314 | Ingredient(name: "Ingredient 2", quantity: 5), 315 | Ingredient(name: "Ingredient 3", quantity: 5) 316 | ], 317 | allergies: [ 318 | Allergie(name: "Allergie 1"), 319 | Allergie(name: "Allergie 2"), 320 | Allergie(name: "Allergie 3") 321 | ], 322 | locations: [ 323 | Location(name: "Location 1", 324 | long: -0.1275, lat: 51.507222), 325 | Location(name: "Location 2", 326 | long: -0.1275, lat: 51.507222), 327 | Location(name: "Location 3", 328 | long: -0.1275, lat: 51.507222), 329 | Location(name: "Location 4", 330 | long: -0.1275, lat: 51.507222) 331 | ]) 332 | ] 333 | 334 | struct Ingredient: Hashable { 335 | let name: String 336 | let quantity: Double 337 | } 338 | 339 | struct Allergie: Hashable { 340 | let name: String 341 | } 342 | 343 | struct Location: Identifiable, Hashable { 344 | var id: String { name } 345 | let name: String 346 | let long: Double 347 | let lat: Double 348 | } 349 | 350 | 351 | struct MenuSection { 352 | let name: String 353 | let items: [any MenuItem] 354 | } 355 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/MenuCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuCardView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 01/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuCardView: View { 11 | 12 | let item: any MenuItem 13 | let didTapAdd: (_ item: any MenuItem) -> Void 14 | 15 | var body: some View { 16 | VStack { 17 | Text(item.name) 18 | .font(.system(size: 100)) 19 | .padding(.bottom, 24) 20 | } 21 | .frame(maxWidth: .infinity) 22 | .frame(height: 200) 23 | .overlay(alignment: .bottom) { 24 | HStack { 25 | VStack(alignment: .leading) { 26 | Text(item.title) 27 | .font(.system(.headline, 28 | design: .rounded, 29 | weight: .bold)) 30 | Text(item.price, 31 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 32 | .font(.caption) 33 | } 34 | Spacer() 35 | Button("Add") { 36 | didTapAdd(item) 37 | } 38 | .controlSize(.small) 39 | .buttonStyle(.borderedProminent) 40 | .buttonBorderShape(.capsule) 41 | } 42 | .padding(8) 43 | .background(.ultraThinMaterial) 44 | } 45 | .background(.white) 46 | .clipShape(RoundedRectangle(cornerRadius: 10)) 47 | .shadow(color: .black.opacity(0.3), radius: 3) 48 | 49 | } 50 | } 51 | 52 | struct MenuCardView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | MenuCardView(item: desserts[1]) { _ in 55 | 56 | } 57 | .padding() 58 | .background(.blue) 59 | .previewLayout(.sizeThatFits) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/MenuGridView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuGridView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuGridView: View { 11 | 12 | @EnvironmentObject private var routerManager: NavigationRouter 13 | @EnvironmentObject private var cartManager: ShoppingCartManager 14 | 15 | let title: String 16 | let items: [any MenuItem] 17 | 18 | private var columns: [GridItem] { 19 | [ GridItem(.adaptive(minimum: 200), spacing: 32) ] 20 | } 21 | 22 | var body: some View { 23 | ScrollView { 24 | LazyVGrid(columns: columns, 25 | spacing: 32) { 26 | ForEach(items, id: \.id) { item in 27 | let route = routerManager.getRoute(for: item) 28 | NavigationLink(value: route) { 29 | MenuCardView(item: item) { item in 30 | cartManager.add(item) 31 | } 32 | } 33 | .buttonStyle(.plain) 34 | } 35 | } 36 | .scenePadding() 37 | } 38 | .navigationTitle(title) 39 | .navigationDestination(for: Route.self) { $0 } 40 | } 41 | } 42 | 43 | struct MenuGridView_Previews: PreviewProvider { 44 | 45 | struct MenuGridViewContainer: View { 46 | 47 | @StateObject var routerManager = NavigationRouter() 48 | @StateObject var fetcher = ProductsFetcher() 49 | @StateObject var cartManager = ShoppingCartManager() 50 | 51 | var body: some View { 52 | NavigationSplitView { 53 | SideBarView(selectedCategory: $routerManager.selectedCategory) 54 | .navigationTitle("Menu") 55 | } detail: { 56 | NavigationStack(path: $routerManager.routes) { 57 | if let category = routerManager.selectedCategory { 58 | let items = fetcher.getItems(in: category) ?? [] 59 | MenuGridView(title: category.title, 60 | items: items) 61 | } 62 | } 63 | } 64 | .environmentObject(cartManager) 65 | .environmentObject(routerManager) 66 | .task { 67 | await fetcher.fetchProducts() 68 | } 69 | } 70 | } 71 | 72 | static var previews: some View { 73 | MenuGridViewContainer() 74 | .preview(for: .iPad) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/MenuItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuItemView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 03/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuItemView: View { 11 | 12 | let item: any MenuItem 13 | 14 | var body: some View { 15 | LabeledContent { 16 | Text(item.price, 17 | format: .currency(code: Locale.current.currency?.identifier ?? "")) 18 | } label: { 19 | Text("\(item.name) \(item.title)") 20 | } 21 | } 22 | } 23 | 24 | struct FoodItemView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | MenuItemView(item: foods[0]) 27 | .previewLayout(.sizeThatFits) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/MenuListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuListView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuListView: View { 11 | 12 | @EnvironmentObject private var routerManager: NavigationRouter 13 | 14 | let title: String 15 | let items: [any MenuItem] 16 | 17 | var body: some View { 18 | List(items, id: \.id) { item in 19 | let route = routerManager.getRoute(for: item) 20 | NavigationLink(value: route) { 21 | MenuItemView(item: item) 22 | } 23 | } 24 | .navigationTitle(title) 25 | .navigationBarTitleDisplayMode(.large) 26 | .navigationDestination(for: Route.self) { $0 } 27 | } 28 | } 29 | 30 | struct MenuListView_Previews: PreviewProvider { 31 | 32 | struct MenuListViewContainer: View { 33 | 34 | @StateObject var routerManager = NavigationRouter() 35 | @StateObject var fetcher = ProductsFetcher() 36 | 37 | var body: some View { 38 | NavigationSplitView { 39 | SideBarView(selectedCategory: $routerManager.selectedCategory) 40 | .navigationTitle("Menu") 41 | } detail: { 42 | NavigationStack(path: $routerManager.routes) { 43 | if let category = routerManager.selectedCategory { 44 | let items = fetcher.getItems(in: category) ?? [] 45 | MenuListView(title: category.title, 46 | items: items) 47 | } 48 | } 49 | } 50 | .environmentObject(routerManager) 51 | .task { 52 | await fetcher.fetchProducts() 53 | } 54 | } 55 | } 56 | 57 | static var previews: some View { 58 | MenuListViewContainer() 59 | .preview(for: .iPhone) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/MenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 03/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routerManager: NavigationRouter 14 | @EnvironmentObject private var fetcher: ProductsFetcher 15 | 16 | @State private var showCart: Bool = false 17 | @State private var showSettings: Bool = false 18 | @State private var showAllColumns: Bool = true 19 | @State private var columVisibility: NavigationSplitViewVisibility = .automatic 20 | 21 | @Binding var layoutExperience: LayoutExperienceSetting? 22 | 23 | var body: some View { 24 | 25 | Group { 26 | 27 | switch layoutExperience { 28 | 29 | case .threeColumn: 30 | ThreeColumnMenuView(columnVisibility: $columVisibility, 31 | showCart: $showCart, 32 | showSettings: $showSettings) 33 | default: 34 | TwoColumnMenuView(columnVisibility: $columVisibility, 35 | showCart: $showCart, 36 | showSettings: $showSettings) 37 | } 38 | } 39 | .sheet(isPresented: $showCart) { 40 | NavigationStack { 41 | CartView() 42 | .environmentObject(cartManager) 43 | } 44 | .presentationDetents([.medium]) 45 | } 46 | .sheet(isPresented: $showSettings) { 47 | NavigationStack { 48 | SettingsView(layoutExperience: $layoutExperience) 49 | } 50 | } 51 | .task { 52 | await fetcher.fetchProducts() 53 | } 54 | .background { 55 | Button("") { 56 | showAllColumns.toggle() 57 | } 58 | .keyboardShortcut("h", modifiers: .control) 59 | .onChange(of: showAllColumns) { newValue in 60 | columVisibility = showAllColumns ? .all : .detailOnly 61 | } 62 | } 63 | } 64 | } 65 | 66 | struct ContentView_Previews: PreviewProvider { 67 | 68 | struct MenuContainerView: View { 69 | 70 | @AppStorage("layoutExperience") 71 | var layoutExperience: LayoutExperienceSetting? 72 | 73 | var body: some View { 74 | MenuView(layoutExperience: $layoutExperience) 75 | .environmentObject(ShoppingCartManager()) 76 | .environmentObject(NavigationRouter()) 77 | .environmentObject(ProductsFetcher()) 78 | } 79 | } 80 | 81 | static var previews: some View { 82 | MenuContainerView() 83 | .preview(for: .iPhone) 84 | 85 | MenuContainerView() 86 | .preview(for: .iPad) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/ThreeColumnMenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreeColumnMenuView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 07/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ThreeColumnMenuView: View { 11 | 12 | 13 | @EnvironmentObject private var cartManager: ShoppingCartManager 14 | @EnvironmentObject private var routerManager: NavigationRouter 15 | @EnvironmentObject private var fetcher: ProductsFetcher 16 | 17 | @Binding var columnVisibility: NavigationSplitViewVisibility 18 | @Binding var showCart: Bool 19 | @Binding var showSettings: Bool 20 | 21 | var body: some View { 22 | 23 | NavigationSplitView(columnVisibility: $columnVisibility) { 24 | switch fetcher.action { 25 | case .loading: 26 | ProgressView() 27 | case .finished: 28 | SideBarView(selectedCategory: $routerManager.selectedCategory) 29 | .navigationTitle("Menu") 30 | .toolbar { 31 | if UIDevice.current.userInterfaceIdiom == .phone { 32 | 33 | ToolbarItem(placement: .primaryAction) { 34 | CartButton(count: cartManager.items.count) { 35 | showCart.toggle() 36 | } 37 | } 38 | 39 | } 40 | } 41 | default: 42 | EmptyView() 43 | } 44 | } content: { 45 | ZStack { 46 | Group { 47 | 48 | if let category = routerManager.selectedCategory { 49 | 50 | let items = fetcher.getItems(in: category) ?? [] 51 | 52 | List(items, 53 | id: \.id, 54 | selection: $routerManager.selectedItemId) { item in 55 | let route = routerManager.getRoute(for: item) 56 | NavigationLink(item.title, value: route) 57 | } 58 | .navigationTitle(category.title) 59 | 60 | 61 | } else { 62 | Text("Choose a category") 63 | } 64 | 65 | } 66 | .toolbar { 67 | ToolbarItem(placement: .primaryAction) { 68 | CartButton(count: cartManager.items.count) { 69 | showCart.toggle() 70 | } 71 | } 72 | 73 | if UIDevice.current.userInterfaceIdiom != .phone { 74 | ToolbarItem { 75 | Button { 76 | showSettings.toggle() 77 | } label: { 78 | Image(systemName: "gear") 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | 86 | } detail: { 87 | 88 | NavigationStack(path: $routerManager.routes) { 89 | 90 | Group { 91 | if let selectedItemId = routerManager.selectedItemId, 92 | let selectedCategory = routerManager.selectedCategory, 93 | let item = fetcher.getItems(in: selectedCategory)?.first(where: { $0.id == selectedItemId }) { 94 | getView(for: item) 95 | } else { 96 | Text("Choose an item") 97 | } 98 | } 99 | .navigationDestination(for: Route.self) { $0 } 100 | } 101 | 102 | } 103 | 104 | } 105 | } 106 | 107 | private extension ThreeColumnMenuView { 108 | 109 | @ViewBuilder 110 | func getView(for item: any MenuItem) -> some View { 111 | if let food = item as? Food { 112 | FoodDetailView(food: food) 113 | } else if let drink = item as? Drink { 114 | DrinkDetailView(drink: drink) 115 | } else if let dessert = item as? Dessert { 116 | DessertDetailView(dessert: dessert) 117 | } else { 118 | EmptyView() 119 | } 120 | } 121 | } 122 | 123 | struct ThreeColumnMenuView_Previews: PreviewProvider { 124 | 125 | struct ThreeColumnMenuContainerView: View { 126 | 127 | @State private var showCart: Bool = false 128 | @State private var showSettings: Bool = false 129 | 130 | @StateObject var routerManager = NavigationRouter() 131 | @StateObject var fetcher = ProductsFetcher() 132 | @StateObject var cartManager = ShoppingCartManager() 133 | 134 | var body: some View { 135 | ThreeColumnMenuView(columnVisibility: .constant(.automatic), 136 | showCart: $showCart, 137 | showSettings: $showSettings) 138 | .environmentObject(routerManager) 139 | .environmentObject(fetcher) 140 | .environmentObject(cartManager) 141 | .task { 142 | await fetcher.fetchProducts() 143 | } 144 | 145 | } 146 | } 147 | 148 | static var previews: some View { 149 | 150 | ThreeColumnMenuContainerView() 151 | .preview(for: .iPhone) 152 | 153 | ThreeColumnMenuContainerView() 154 | .preview(for: .iPad) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Menu/Views/TwoColumnMenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwoColumnMenuView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 06/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TwoColumnMenuView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routerManager: NavigationRouter 14 | @EnvironmentObject private var fetcher: ProductsFetcher 15 | 16 | @Binding var columnVisibility: NavigationSplitViewVisibility 17 | @Binding var showCart: Bool 18 | @Binding var showSettings: Bool 19 | 20 | var body: some View { 21 | 22 | NavigationSplitView(columnVisibility: $columnVisibility) { 23 | 24 | Group { 25 | switch fetcher.action { 26 | case .loading: 27 | ProgressView() 28 | case .finished: 29 | SideBarView(selectedCategory: $routerManager.selectedCategory) 30 | default: 31 | EmptyView() 32 | } 33 | } 34 | .navigationTitle("Menu") 35 | .toolbar { 36 | ToolbarItem(placement: .primaryAction) { 37 | CartButton(count: cartManager.items.count) { 38 | showCart.toggle() 39 | } 40 | } 41 | 42 | if UIDevice.current.userInterfaceIdiom != .phone { 43 | ToolbarItem { 44 | Button { 45 | showSettings.toggle() 46 | } label: { 47 | Image(systemName: "gear") 48 | } 49 | } 50 | } 51 | } 52 | } detail: { 53 | if let category = routerManager.selectedCategory { 54 | 55 | let items = fetcher.getItems(in: category) ?? [] 56 | 57 | NavigationStack(path: $routerManager.routes) { 58 | switch UIDevice.current.userInterfaceIdiom { 59 | case .phone: 60 | MenuListView(title: category.title, 61 | items: items) 62 | .toolbar { 63 | ToolbarItem(placement: .primaryAction) { 64 | CartButton(count: cartManager.items.count) { 65 | showCart.toggle() 66 | } 67 | } 68 | } 69 | default: 70 | MenuGridView(title: category.title, 71 | items: items) 72 | } 73 | } 74 | } else { 75 | Text("Choose a category") 76 | } 77 | } 78 | } 79 | } 80 | 81 | struct TwoColumnMenuView_Previews: PreviewProvider { 82 | 83 | struct TwoColumnMenuContainerView: View { 84 | 85 | @State private var showCart: Bool = false 86 | @State private var showSettings: Bool = false 87 | 88 | @StateObject var routerManager = NavigationRouter() 89 | @StateObject var fetcher = ProductsFetcher() 90 | @StateObject var cartManager = ShoppingCartManager() 91 | 92 | var body: some View { 93 | TwoColumnMenuView(columnVisibility: .constant(.automatic), 94 | showCart: $showCart, 95 | showSettings: $showSettings) 96 | .environmentObject(routerManager) 97 | .environmentObject(fetcher) 98 | .environmentObject(cartManager) 99 | .task { 100 | await fetcher.fetchProducts() 101 | } 102 | } 103 | } 104 | 105 | static var previews: some View { 106 | TwoColumnMenuContainerView() 107 | .preview(for: .iPhone) 108 | 109 | TwoColumnMenuContainerView() 110 | .preview(for: .iPad) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/NavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRouter.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 07/02/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | final class NavigationRouter: ObservableObject { 12 | 13 | @Published var routes = [Route]() 14 | @Published var selectedCategory: MenuCategory? 15 | @Published var selectedItemId: String? 16 | 17 | func push(to screen: Route) { 18 | guard !routes.contains(screen) else { 19 | return 20 | } 21 | routes.append(screen) 22 | } 23 | 24 | func goBack() { 25 | _ = routes.popLast() 26 | } 27 | 28 | func reset() { 29 | routes = [] 30 | } 31 | 32 | func replace(stack: [Route]) { 33 | routes = stack 34 | } 35 | 36 | func getRoute(for item: any MenuItem) -> Route? { 37 | switch item { 38 | case is Food: 39 | return Route.menuItem(item: item as! Food) 40 | case is Drink: 41 | return Route.menuItem(item: item as! Drink) 42 | case is Dessert: 43 | return Route.menuItem(item: item as! Dessert) 44 | default: 45 | return nil 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/ProductsFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsFetcher.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 09/02/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | // Data 12 | let products = [ 13 | MenuSection(name: "Foods", 14 | items: foods), 15 | MenuSection(name: "Drinks", 16 | items: drinks), 17 | MenuSection(name: "Desserts", 18 | items: desserts), 19 | ] 20 | 21 | @MainActor 22 | final class ProductsFetcher: ObservableObject { 23 | 24 | enum Action { 25 | case loading 26 | case finished(items: [MenuSection]) 27 | } 28 | 29 | @Published private(set) var action: Action? 30 | 31 | func fetchProducts() async { 32 | 33 | guard action == nil else { 34 | return 35 | } 36 | 37 | action = .loading 38 | 39 | // 2 second delay 40 | let duration = UInt64(2 * 1_000_000_000) 41 | try? await Task.sleep(nanoseconds: duration) 42 | 43 | action = .finished(items: products) 44 | } 45 | 46 | func fetchProduct(by id: String) async -> (any MenuItem)? { 47 | 48 | let duration = UInt64(0.5 * 1_000_000_000) 49 | try? await Task.sleep(nanoseconds: duration) 50 | 51 | let item = products 52 | .flatMap { $0.items } 53 | .first(where: { $0.id == id }) 54 | 55 | return item 56 | } 57 | 58 | func getItems(in category: MenuCategory?) -> [any MenuItem]? { 59 | 60 | guard case let .finished(data) = action, 61 | let category = category else { return [] } 62 | switch category { 63 | case .food: 64 | return data.first { $0.items.allSatisfy { $0 is Food } }?.items 65 | case .drink: 66 | return data.first { $0.items.allSatisfy { $0 is Drink } }?.items 67 | case .dessert: 68 | return data.first { $0.items.allSatisfy { $0 is Dessert } }?.items 69 | } 70 | } 71 | 72 | func getCategory(for item: any MenuItem) -> MenuCategory? { 73 | switch item { 74 | case is Food: 75 | return .food 76 | case is Drink: 77 | return .drink 78 | case is Dessert: 79 | return .dessert 80 | default: 81 | return nil 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Promo/Views/PromoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromoView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 09/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PromoView: View { 11 | 12 | @EnvironmentObject private var cartManager: ShoppingCartManager 13 | @EnvironmentObject private var routeManager: NavigationRouter 14 | 15 | let data: PromoData 16 | 17 | var body: some View { 18 | VStack { 19 | if let pct = data.pct { 20 | Text(pct, format: .percent) 21 | .padding(50) 22 | .font(.system(size: 60, 23 | weight: .heavy, 24 | design: .rounded)) 25 | .background( 26 | Circle().fill(.red) 27 | ) 28 | .foregroundColor(.white) 29 | } 30 | 31 | Text("Get this **Great Offer** 🔥🔥🔥") 32 | .font(.title2) 33 | Text(data.desc) 34 | .italic() 35 | if data.pct != nil { 36 | Button("Apply Discount") { 37 | cartManager.set(promo: data) 38 | routeManager.replace(stack: [.cart]) 39 | } 40 | .controlSize(.large) 41 | .buttonStyle(.bordered) 42 | .padding(.top, 16) 43 | } 44 | 45 | } 46 | .multilineTextAlignment(.center) 47 | .padding() 48 | } 49 | } 50 | 51 | struct PromoView_Previews: PreviewProvider { 52 | static var previews: some View { 53 | PromoView(data: .init(desc: "Great deal", pct: 0.5)) 54 | .environmentObject(NavigationRouter()) 55 | .environmentObject(ShoppingCartManager()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Route.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Route.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 08/02/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct PromoData { 12 | let desc: String 13 | let pct: Decimal? 14 | } 15 | 16 | enum Route { 17 | case menuItem(item: any MenuItem, hideTabBar: Bool = false) 18 | case cart 19 | case ingredients(items: [Ingredient]) 20 | case allergies(items: [Allergie]) 21 | case locations(places: [Location]) 22 | case map(location: Location) 23 | case promo(data: PromoData, hideTabBar: Bool = false) 24 | case invalidProduct(hideTabBar: Bool = false) 25 | } 26 | 27 | extension Route { 28 | 29 | static func buildDeepLink(from route: Route) -> URL? { 30 | switch route { 31 | case .menuItem(let item, _): 32 | 33 | let queryProductItem = item.title.replacingOccurrences(of: " ", with: "+") 34 | let queryProductId = "\(item.name)_\(queryProductItem)" 35 | 36 | var url = URL(string: "myfoodapp://product")! 37 | let queryItems = [URLQueryItem(name: "item", value: queryProductId)] 38 | 39 | url.append(queryItems: queryItems) 40 | 41 | return url 42 | default: 43 | break 44 | } 45 | 46 | return nil 47 | } 48 | } 49 | 50 | extension Route: Hashable { 51 | 52 | func hash(into hasher: inout Hasher) { 53 | hasher.combine(self.hashValue) 54 | } 55 | 56 | static func == (lhs: Route, rhs: Route) -> Bool { 57 | switch (lhs, rhs) { 58 | case (.menuItem(let lhsItem, _), .menuItem(let rhsItem, _)): 59 | return lhsItem.id == rhsItem.id 60 | case (.cart, .cart): 61 | return true 62 | case (.ingredients(let lhsItem), .ingredients(let rhsItem)): 63 | return lhsItem == rhsItem 64 | case (.allergies(let lhsItem), .allergies(let rhsItem)): 65 | return lhsItem == rhsItem 66 | case (.locations(let lhsItem), .locations(let rhsItem)): 67 | return lhsItem == rhsItem 68 | case (.map(let lhsItem), .map(let rhsItem)): 69 | return lhsItem.id == rhsItem.id 70 | case (.promo, .promo): 71 | return true 72 | case (.invalidProduct, .invalidProduct): 73 | return true 74 | default: 75 | return false 76 | } 77 | } 78 | 79 | } 80 | 81 | extension Route: View { 82 | 83 | var body: some View { 84 | 85 | switch self { 86 | 87 | case .menuItem(let item, let hideTabBar): 88 | 89 | Group { 90 | switch item { 91 | 92 | case is Food: 93 | FoodDetailView(food: item as! Food) 94 | case is Drink: 95 | DrinkDetailView(drink: item as! Drink) 96 | case is Dessert: 97 | DessertDetailView(dessert: item as! Dessert) 98 | default: 99 | EmptyView() 100 | } 101 | } 102 | .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar) 103 | case .cart: 104 | CartView() 105 | case .ingredients(let items): 106 | IngredientsDetailView(ingredients: items) 107 | case .allergies(let items): 108 | AllergiesDetailView(allergies: items) 109 | case .locations(let places): 110 | LocationsDetailView(locations: places) 111 | case .map(let location): 112 | LocationMapView(location: location) 113 | case .promo(let data, let hideTabBar): 114 | PromoView(data: data) 115 | .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar) 116 | case .invalidProduct(let hideTabBar): 117 | InvalidProductView() 118 | .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/RouteFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteFinder.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 09/02/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DeepLinkURLs: String { 11 | case promo = "promo" 12 | case product = "product" 13 | } 14 | 15 | struct RouteFinder { 16 | 17 | func find(from url: URL, productsFetcher: ProductsFetcher) async -> Route? { 18 | 19 | guard let host = url.host() else { return nil } 20 | 21 | switch DeepLinkURLs(rawValue: host) { 22 | case .promo: 23 | 24 | let queryParams = url.queryParameters 25 | guard let descQueryVal = queryParams?["desc"] as? String else { return nil } 26 | let discountQueryVal = queryParams?["discount"] as? String ?? "" 27 | let discountPct = Decimal(string: discountQueryVal) ?? nil 28 | 29 | let promoData = PromoData(desc: descQueryVal, pct: discountPct) 30 | return .promo(data: promoData, hideTabBar: true) 31 | 32 | case .product: 33 | 34 | let queryParams = url.queryParameters 35 | guard let itemQueryVal = queryParams?["item"] as? String, 36 | let product = await productsFetcher.fetchProduct(by: itemQueryVal) else { return .invalidProduct(hideTabBar: true) } 37 | 38 | return .menuItem(item: product, hideTabBar: true) 39 | default: 40 | return nil 41 | 42 | } 43 | } 44 | } 45 | 46 | extension URL { 47 | public var queryParameters: [String: String]? { 48 | guard 49 | let components = URLComponents(url: self, resolvingAgainstBaseURL: true), 50 | let queryItems = components.queryItems else { return nil } 51 | return queryItems.reduce(into: [String: String]()) { (result, item) in 52 | result[item.name] = item.value?.replacingOccurrences(of: "+", with: " ") 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Settings/Model/LayoutExperienceSetting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutExperienceSetting.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 04/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LayoutExperienceSetting: Int, Identifiable, CaseIterable, Equatable { 11 | 12 | var id: Int { rawValue } 13 | 14 | case twoColumn 15 | case threeColumn 16 | 17 | var imageName: String { 18 | switch self { 19 | case .twoColumn: 20 | return "sidebar.left" 21 | case .threeColumn: 22 | return "rectangle.split.3x1" 23 | } 24 | } 25 | 26 | var title: String { 27 | switch self { 28 | case .twoColumn: 29 | return "Two columns" 30 | case .threeColumn: 31 | return "Three columns" 32 | } 33 | } 34 | 35 | var description: String { 36 | switch self { 37 | case .twoColumn: 38 | return "Presents views in two columns: sidebar and detail." 39 | case .threeColumn: 40 | return "Presents views in three columns: sidebar, content, and detail." 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Settings/NotificationsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsManager.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 11/02/2023. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | import UIKit 11 | 12 | @MainActor 13 | class NotificationsManager: ObservableObject { 14 | 15 | @Published private(set) var hasPermission = false 16 | 17 | init() { 18 | Task { 19 | await getAuthStatus() 20 | } 21 | } 22 | 23 | func request() async { 24 | 25 | do { 26 | hasPermission = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) 27 | if hasPermission { 28 | UIApplication.shared.registerForRemoteNotifications() 29 | } 30 | } catch { 31 | print(error) 32 | } 33 | } 34 | 35 | func getAuthStatus() async { 36 | let status = await UNUserNotificationCenter.current().notificationSettings() 37 | switch status.authorizationStatus { 38 | 39 | case .authorized, 40 | .provisional, 41 | .ephemeral: 42 | hasPermission = true 43 | default: 44 | hasPermission = false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Settings/View/LayoutExperienceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutExperienceView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 04/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LayoutExperienceView: View { 11 | 12 | @Binding var selectedLayoutExperience: LayoutExperienceSetting? 13 | 14 | var columns: [GridItem] { 15 | [ 16 | GridItem(.flexible(), spacing: 10), 17 | GridItem(.flexible(), spacing: 10) 18 | ] 19 | } 20 | 21 | var body: some View { 22 | VStack(alignment: .leading, 23 | spacing: 24) { 24 | Text("Layout Configuration") 25 | .font(.title) 26 | .bold() 27 | 28 | LazyVGrid(columns: columns) { 29 | ForEach(LayoutExperienceSetting.allCases) { item in 30 | Button { 31 | selectedLayoutExperience = item 32 | } label: { 33 | LayoutExperienceSelectionView(selectedItem: $selectedLayoutExperience, 34 | item: item) 35 | } 36 | } 37 | } 38 | } 39 | .padding() 40 | .background { 41 | RoundedRectangle(cornerRadius: 10, style: .continuous) 42 | .fill(.thinMaterial) 43 | } 44 | .scenePadding() 45 | } 46 | } 47 | 48 | struct LayoutExperienceSelectionView: View { 49 | 50 | @State private var isHovering = false 51 | @Binding var selectedItem: LayoutExperienceSetting? 52 | 53 | let item: LayoutExperienceSetting 54 | 55 | var body: some View { 56 | VStack { 57 | Image(systemName: item.imageName) 58 | .font(.largeTitle) 59 | .foregroundStyle(shapeStyle(Color.accentColor)) 60 | VStack { 61 | Text(item.title) 62 | .bold() 63 | .foregroundStyle(shapeStyle(Color.primary)) 64 | Text(item.description) 65 | .lineLimit(3, reservesSpace: true) 66 | .font(.callout) 67 | .foregroundStyle(shapeStyle(Color.secondary)) 68 | } 69 | .padding(.top, 16) 70 | } 71 | .padding(24) 72 | .background { 73 | RoundedRectangle(cornerRadius: 12, 74 | style: .continuous) 75 | .fill(selectedItem == item ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.background)) 76 | .shadow(radius: selectedItem == item ? 4 : 0) 77 | RoundedRectangle(cornerRadius: 12, style: .continuous) 78 | .stroke(isHovering ? Color.accentColor : .clear) 79 | } 80 | .scaleEffect(isHovering ? 1.02 : 1) 81 | .onHover { isHovering in 82 | withAnimation { 83 | self.isHovering = isHovering 84 | } 85 | } 86 | } 87 | 88 | func shapeStyle(_ style: S) -> some ShapeStyle { 89 | if selectedItem == item { 90 | return AnyShapeStyle(.background) 91 | } else { 92 | return AnyShapeStyle(style) 93 | } 94 | } 95 | } 96 | 97 | struct LayoutExperienceView_Previews: PreviewProvider { 98 | static var previews: some View { 99 | LayoutExperienceView(selectedLayoutExperience: .constant(.twoColumn)) 100 | .padding() 101 | .previewLayout(.sizeThatFits) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Settings/View/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 11/02/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | 12 | @StateObject private var manager = NotificationsManager() 13 | 14 | @Binding var layoutExperience: LayoutExperienceSetting? 15 | 16 | var body: some View { 17 | ScrollView { 18 | VStack { 19 | 20 | if UIDevice.current.userInterfaceIdiom != .phone { 21 | LayoutExperienceView(selectedLayoutExperience: $layoutExperience) 22 | } 23 | 24 | Button("Request Notification\n Permission") { 25 | Task { 26 | await manager.request() 27 | await manager.getAuthStatus() 28 | } 29 | } 30 | .frame(maxWidth: .infinity) 31 | .buttonStyle(.bordered) 32 | .disabled(manager.hasPermission) 33 | .task { 34 | await manager.getAuthStatus() 35 | } 36 | } 37 | } 38 | .navigationTitle("Settings") 39 | } 40 | } 41 | 42 | struct SettingsView_Previews: PreviewProvider { 43 | 44 | struct SettingsContainerView: View { 45 | 46 | @AppStorage("layoutExperience") 47 | var layoutExperience: LayoutExperienceSetting? 48 | 49 | var body: some View { 50 | NavigationStack { 51 | SettingsView(layoutExperience: $layoutExperience) 52 | } 53 | } 54 | } 55 | 56 | static var previews: some View { 57 | SettingsContainerView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Sidebar/Model/MenuCategory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuCategory.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 05/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MenuCategory: Int, CaseIterable, Identifiable { 11 | 12 | var id: Int { rawValue } 13 | 14 | case food 15 | case dessert 16 | case drink 17 | } 18 | 19 | extension MenuCategory { 20 | 21 | var title: String { 22 | switch self { 23 | case .food: 24 | return "Food" 25 | case .dessert: 26 | return "Desserts" 27 | case .drink: 28 | return "Drinks" 29 | } 30 | } 31 | 32 | var systemImage: String { 33 | switch self { 34 | case .food: 35 | return "fork.knife" 36 | case .dessert: 37 | return "birthday.cake" 38 | case .drink: 39 | return "mug" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStack/Sidebar/View/SideBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideBarView.swift 3 | // Introduction to NavigationStack 4 | // 5 | // Created by Tunde Adegoroye on 05/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SideBarView: View { 11 | 12 | @Binding var selectedCategory: MenuCategory? 13 | 14 | var body: some View { 15 | List(MenuCategory.allCases, 16 | selection: $selectedCategory) { category in 17 | NavigationLink(value: category) { 18 | Label(category.title, systemImage: category.systemImage) 19 | } 20 | } 21 | } 22 | } 23 | 24 | struct SideBarView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | NavigationSplitView { 27 | SideBarView(selectedCategory: .constant(.food)) 28 | } detail: { 29 | Text("Some Content Here") 30 | } 31 | .preview(for: .iPhone) 32 | 33 | NavigationSplitView { 34 | SideBarView(selectedCategory: .constant(.drink)) 35 | } detail: { 36 | Text("Some Content Here") 37 | } 38 | .preview(for: .iPad) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStackTests/Introduction_to_NavigationStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Introduction_to_NavigationStackTests.swift 3 | // Introduction to NavigationStackTests 4 | // 5 | // Created by Tunde Adegoroye on 08/02/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class Introduction_to_NavigationStackTests: 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 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStackTests/RouteFinderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteFinderTests.swift 3 | // Introduction to NavigationStackTests 4 | // 5 | // Created by Tunde Adegoroye on 11/02/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import Introduction_to_NavigationStack 10 | 11 | @MainActor 12 | final class RouteFinderTests: XCTestCase { 13 | 14 | private var sut: RouteFinder! 15 | private var fetcher: ProductsFetcher! 16 | 17 | override func setUp() { 18 | sut = RouteFinder() 19 | fetcher = ProductsFetcher() 20 | } 21 | 22 | override func tearDown() { 23 | sut = nil 24 | fetcher = nil 25 | } 26 | 27 | func testPromoLinkRouteIsParsedCorrectly() async throws { 28 | 29 | let deeplinkURL = try XCTUnwrap(URL(string: "myfoodapp://promo?discount=0.3&desc=enjoy+this+great+discount")) 30 | let route = await sut.find(from: deeplinkURL, productsFetcher: fetcher) 31 | let promoData = PromoData(desc: "enjoy this great discount", pct: 0.3) 32 | XCTAssertEqual(route, .promo(data: promoData, hideTabBar: true), "This should be a valid path to a promotion") 33 | } 34 | 35 | func testPromoLinkRouteWithoutDescIsNotParsedCorrectly() async throws { 36 | 37 | let deeplinkURL = try XCTUnwrap(URL(string: "myfoodapp://promo?discount=0.3")) 38 | let route = await sut.find(from: deeplinkURL, productsFetcher: fetcher) 39 | XCTAssertNil(route, "This should be nil since it's missing a description") 40 | } 41 | 42 | func testProductLinkRouteIsParsedCorrectly() async throws { 43 | let iceCream = try XCTUnwrap(desserts.first(where: { $0.id == "🍦_Ice Cream" })) 44 | let deeplinkURL = try XCTUnwrap(Route.buildDeepLink(from: .menuItem(item: iceCream))) 45 | let route = await sut.find(from: deeplinkURL, productsFetcher: fetcher) 46 | XCTAssertEqual(route, .menuItem(item: iceCream, hideTabBar: true), "The route should be to ice cream") 47 | } 48 | 49 | func testInvalidLinkIsNotParsedCorrectly() async throws { 50 | 51 | let url = try XCTUnwrap(URL(string: "myfoodapp://product?item=xxx")) 52 | let route = await sut.find(from: url, productsFetcher: fetcher) 53 | XCTAssertEqual(route, .invalidProduct(hideTabBar: true),"The product path should be invalid product") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Project/Introduction to NavigationStackTests/RouteModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteModelTests.swift 3 | // Introduction to NavigationStackTests 4 | // 5 | // Created by Tunde Adegoroye on 08/02/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import Introduction_to_NavigationStack 10 | 11 | final class RouteModelTests: XCTestCase { 12 | 13 | private var sut: NavigationRouter! 14 | 15 | override func setUp() { 16 | sut = NavigationRouter() 17 | } 18 | 19 | override func tearDown() { 20 | sut = nil 21 | } 22 | 23 | func testRoutesIsEmptyOnInitialisation() { 24 | 25 | XCTAssertEqual(sut.routes.count, 0, "There should be no routes in the stack") 26 | 27 | } 28 | 29 | func testPushingOneScreenHasOneRoute() { 30 | 31 | sut.push(to: .menuItem(item: foods[0])) 32 | XCTAssertEqual(sut.routes.count, 1, "There should be a route in the stack") 33 | } 34 | 35 | func testPushingTwoScreensHasTwoRoutes() { 36 | 37 | sut.push(to: .menuItem(item: foods[0])) 38 | sut.push(to: .cart) 39 | XCTAssertEqual(sut.routes.count, 2, "There should be 2 routes in the stack") 40 | 41 | } 42 | 43 | func testResettingRoutesHasNoRoutes() { 44 | 45 | sut.push(to: .menuItem(item: foods[0])) 46 | sut.push(to: .cart) 47 | sut.reset() 48 | XCTAssertEqual(sut.routes.count, 0, "There should be 0 routes in the stack") 49 | } 50 | 51 | func testGoingBackHasOneRoute() { 52 | 53 | sut.push(to: .menuItem(item: foods[0])) 54 | sut.push(to: .cart) 55 | sut.goBack() 56 | XCTAssertEqual(sut.routes.count, 1, "There should be 1 route in the stack") 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Project/Introduction-to-NavigationStack-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLIconFile 11 | 12 | CFBundleURLName 13 | com.tundsdev.Introduction-to-NavigationStack 14 | CFBundleURLSchemes 15 | 16 | myfoodapp 17 | 18 | 19 | 20 | 21 | FirebaseAppDelegateProxyEnabled 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Foods App) SwiftUI Multiplatform & Navigation Sample App 2 | 3 | ## Introduction 4 | 5 | This repo a 100% SwiftUI app showcasing examples of navigation, Multiplatform support & deep linking using the new NavigationStack & NavigationSplitView API's based of my [course](https://youtube.com/playlist?list=PLvUWi5tdh92wWS3F-AVsCJHkhBlrkBp6f) that that showcases everything you need to know about handling navigation in SwiftUI apps. 6 | 7 | ![Everything You Need To Know About Requesting App Reviews Ratings In SwiftUI Copy 8](https://user-images.githubusercontent.com/13179531/229338407-351d1ba7-bf56-4fd7-a57d-db96fffcc34b.png) 8 | 9 | ## Requirements 10 | 11 | * Xcode 14.2+ 12 | * Swift 5.7+ 13 | * SwiftUI 4.0+ 14 | * iOS16.1+ 15 | 16 | ## Preview 17 | 18 | | Platform | Preview | 19 | | ----------- | ----------- | 20 | | iPhone || 21 | | iPad || 22 | | Mac || 23 | 24 | ## Features 25 | 26 | * Simple & Organised Project structure 27 | * iPhone, iPad & Mac support 28 | * Handling Sheet presentation using [detents](https://developer.apple.com/documentation/swiftui/modal-presentations) 29 | * Examples of handling navigation using [`NavigationStack`](https://developer.apple.com/documentation/swiftui/navigationstack) & [`navigationDestination(for:destination:)`](https://developer.apple.com/documentation/swiftui/view/navigationdestination(for:destination:)) 30 | * Handling navigation programatically by building a RouteManager 31 | * Custom URL Scheme Integration for deep links 32 | * How to handle deeplinking in SwiftUI for multiplatform & route to screens 33 | * Firebase integration using SPM 🚨**Replace GoogleService's Plist with your own**🚨 34 | * Push Notification support using FCM [(Firebase Cloud Messaging)](https://firebase.google.com/docs/cloud-messaging/ios/client) 35 | * How to handle push notification deep links 36 | * Adapative iPad & Mac layout for 2 & 3 column layouts 37 | * [`keyboardShortcut(_:)`](https://developer.apple.com/documentation/swiftui/capsule/keyboardshortcut(_:)-1vqvs/) actions to toggle side bar visibility 38 | * Apple Pencil actions using [`onHover`](https://developer.apple.com/documentation/swiftui/path/onhover(perform:)/) for animating hover effects for iPad & Mac 39 | * Unit Tests 40 | 41 | ## Project Installation 42 | 43 | 1. Clone the repository using git clone https://github.com/tunds/SwiftUI-Navigation-Multiplatform-Example.git or use Open in Xcode 44 | 2. Open the project in Xcode 45 | 3. Build and run the app on a simulator or device for either iPhone, iPad or MacOS 46 | 47 | 48 | ## Deep Links Setup Guide (Custom URL Schemes) 49 | 50 | In order get deep links working in the sample project you could either use the default stock app on the device, terminal or a handy tool like [RocketSim](https://www.rocketsim.app). You can check out this chapter in my video to learn more about testing deep links on a device [here](https://youtu.be/KevGhZQRcG8?t=283). 51 | 52 | Below is a table of all the current deeplinks the application handles routing for and will navigate to. 53 | 54 | | Name | Example | Purpose | Condition | 55 | | ----------- | ----------- | ----------- | ----------- | 56 | | Invalid Product | myfoodapp://product?item=xxxx | Handles showing an invalid product screen for when the app can't find a product with the specified id. | For the item key query parameter add in an invalid product id | 57 | | Open Item | myfoodapp://product?item=🍦_Ice+Cream | Handles showing a product that has been defined, in the dummy data model. | For the item key query parameter the id is a combination of the title and name, all the valid ids can be found in `Food.swift` and it's normally a combination of `_`. Any spaces are replaced with a +. | 58 | | Promo | myfoodapp://promo?discount=0.3&desc=enjoy+this+discount | Handles showing a promotion to the user. | For the discount key query parameter the value should be a decimal represented in a string representation and the desc key has a value of a string, any space should be replaced with a + | 59 | | Open App | myfoodapp://menu | Opens up the app | There are no query parameters for this since this just opens up the app using a generic url scheme | 60 | 61 | ## Push Notification Setup Guide (FCM) 62 | 63 | This project uses [FCM](https://firebase.google.com/docs/cloud-messaging/ios/client) for handling push notifications. This project already has Firebase added as a Swift Package. But you will need to replace the GoogleServices plist file with your own if you plan to connect this to your own Firebase project. A guide on this can be seen [here](https://youtu.be/l-iN0kY_bmg). 64 | 65 | Once you do this you can then setup your firebase project by uploading either an APNS Key or Certificates. You can learn more about this [here](https://youtu.be/msWb_Iyscro?t=2851). 66 | 67 | The code for push notification deep links is reusing code and the same deep links defined in the table in [Deep Links Setup Guide (Custom URL Schemes)](#deep-links-setup-guide-(custom-url-schemes)). In order to send a deep link in firebase make sure the key you use is `link` and you copy one of the deeplinks defined above. 68 | 69 | ## Credits 70 | 71 | This demo app was created me [tundsdev](https://www.youtube.com/channel/UC7AuV86ZjR3YaEdb5USNvWQ). And is part of a course which you can view in an playlist on my channel [here](https://youtube.com/playlist?list=PLvUWi5tdh92wWS3F-AVsCJHkhBlrkBp6f). This playlist essentially breaks down all the code that exists in this playlist and everything you need to know. I would also highly recommend you check out these videos to learn more about my thought process and reasoning. Below are links to each video in this course which builds up this project. 72 | 73 | * [**Become A SwiftUI Navigation Pro (Navigation Course)**](https://youtube.com/playlist?list=PLvUWi5tdh92wWS3F-AVsCJHkhBlrkBp6f) 74 | * [**Unlock the Secrets of SwiftUI NavigationStack: A Beginners Guide 🔐**](https://youtu.be/cik3doGHZiI) 75 | * [**Discover Programmatic Navigation in SwiftUI 🌍 | SwiftUI NavigationStack Tutorial**](https://youtu.be/GpjTeGPgIs8) 76 | * [**Learn How To Easily Create Deep Links in SwiftUI 🔗**](https://youtu.be/KevGhZQRcG8) 77 | * [**The Absolute Beginner's Guide to iOS Firebase Push Notifications ⚡️**](https://youtu.be/msWb_Iyscro) 78 | * [**SwiftUI Multiplatform Magic 🪄: Your First Multiplatform App using NavigationSplitView**](https://youtu.be/v7rRbiDprIg) 79 | 80 | 81 | ## Credits 82 | 83 | <a href='https://ko-fi.com/S6S83GEBY' target='_blank'><img height='50' style='border:0px;height:50px;' src='https://cdn.ko-fi.com/cdn/kofi4.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com'/></a> 84 | 85 | If you're interested in following me you can find me on: 86 | 87 | * [YouTube](https://www.youtube.com/channel/UC7AuV86ZjR3YaEdb5USNvWQ) 88 | * [Twitter](https://twitter.com/tundsdev) 89 | * [Mastodon](https://iosdev.space/@tundsdev) 90 | * [Website](https://tunds.dev) 91 | 92 | I also have a [Kofi](https://ko-fi.com/tundsdev) where you can donate and buy me a coffee if you found this useful also. 93 | 94 | ## 📄 License 95 | 96 | Copyright © 2023 Tunde Adegoroye 97 | 98 | License: [MIT](LICENSE) 99 | --------------------------------------------------------------------------------