├── MVI-SwiftUI.xcodeproj ├── .xcodesamplecode.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ ├── vansimov.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ ├── viacheslav.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── vyacheslavansimov.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── vansimov.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── viacheslav.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── vyacheslavansimov.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── MVI-SwiftUI.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── viacheslav.xcuserdatad │ ├── UserInterfaceState.xcuserstate │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── MVI-SwiftUI ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Core │ ├── Services │ │ └── WWDCUrl │ │ │ ├── Data │ │ │ └── SwiftUIData.plist │ │ │ ├── WWDCUrlService.swift │ │ │ └── WWDCUrlServiceProtocol.swift │ └── Utilites │ │ └── MVIContainer.swift ├── DesingSystem │ └── Modifiers │ │ └── Loading │ │ ├── LoadingModifier.swift │ │ └── View+LoadingModifier.swift ├── Info.plist ├── MVI_SwiftUIApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Screens │ ├── Item │ ├── Intent │ │ ├── DataModels │ │ │ └── ItemExternalData.swift │ │ ├── ItemIntent.swift │ │ └── ItemIntentProtocol.swift │ ├── Model │ │ ├── ItemModel.swift │ │ └── ItemModelProtocol.swift │ ├── Router │ │ ├── ItemRouter.swift │ │ └── ItemRouterDelegate.swift │ └── View │ │ └── ItemView.swift │ └── List │ ├── Intent │ ├── DataModels │ │ └── ListExternalData.swift │ ├── ListIntent.swift │ └── ListIntentProtocol.swift │ ├── Model │ ├── DataModels │ │ └── ListModelError.swift │ ├── ListModel.swift │ └── ListModelPotocols.swift │ ├── Router │ ├── ListRouter.swift │ └── ListRouterDelegate.swift │ └── View │ ├── Custom Elements │ └── ListUrlContentView.swift │ └── ListView.swift ├── MyLibraries └── RouterModifier │ ├── .swiftpm │ └── xcode │ │ ├── package.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── viacheslav.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcuserdata │ │ └── viacheslav.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── RouterModifier │ │ ├── Core │ │ ├── Modifier │ │ │ ├── RouterAlertModifier.swift │ │ │ ├── RouterDismissModifier.swift │ │ │ ├── RouterNavigationDestinationModifier.swift │ │ │ ├── RouterNavigationLinkModifier.swift │ │ │ └── RouterSheetModifier.swift │ │ └── RouterModifierProtocol+Body.swift │ │ ├── HelperClasses │ │ ├── RouterDefaultAlert.swift │ │ ├── RouterEmptyScreen.swift │ │ ├── RouterEvents.swift │ │ ├── RouterScreenPresentationType.swift │ │ └── RouterScreenProtocol.swift │ │ └── RouterModifierProtocol.swift │ └── Tests │ └── RouterModifierTests │ └── RouterModifierTests.swift ├── README.md ├── README_sources ├── image_001.jpeg ├── image_002.png ├── image_003.png └── image_004.png └── Templates_for_Xcode └── xctemplate ├── SwiftUI MVI+Router.xctemplate ├── TemplateIcon.pdf ├── TemplateInfo.plist ├── ___FILEBASENAME___ExternalData.swift ├── ___FILEBASENAME___Intent.swift ├── ___FILEBASENAME___IntentProtocol.swift ├── ___FILEBASENAME___Model.swift ├── ___FILEBASENAME___ModelProtocol.swift ├── ___FILEBASENAME___Router.swift ├── ___FILEBASENAME___RouterDelegate.swift └── ___FILEBASENAME___View.swift └── SwiftUI MVI.xctemplate ├── TemplateIcon.pdf ├── TemplateInfo.plist ├── ___FILEBASENAME___ExternalData.swift ├── ___FILEBASENAME___Intent.swift ├── ___FILEBASENAME___IntentProtocol.swift ├── ___FILEBASENAME___Model.swift ├── ___FILEBASENAME___ModelProtocol.swift └── ___FILEBASENAME___View.swift /MVI-SwiftUI.xcodeproj/.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13446D2E2AD1308F0081E7F3 /* RouterModifier in Frameworks */ = {isa = PBXBuildFile; productRef = 13446D2D2AD1308F0081E7F3 /* RouterModifier */; }; 11 | 13DF5D712C1AD18800FFE5AC /* ListModelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D702C1AD18800FFE5AC /* ListModelError.swift */; }; 12 | 13DF5D742C1AD1D600FFE5AC /* ListExternalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D732C1AD1D600FFE5AC /* ListExternalData.swift */; }; 13 | 13DF5D782C1AD45E00FFE5AC /* LoadingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D772C1AD45E00FFE5AC /* LoadingModifier.swift */; }; 14 | 13DF5D7B2C1AD72B00FFE5AC /* View+LoadingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D7A2C1AD72B00FFE5AC /* View+LoadingModifier.swift */; }; 15 | 13DF5D7E2C1AE53100FFE5AC /* ItemExternalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D7D2C1AE53100FFE5AC /* ItemExternalData.swift */; }; 16 | 13DF5D842C1B174F00FFE5AC /* ListRouterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D832C1B174F00FFE5AC /* ListRouterDelegate.swift */; }; 17 | 13DF5D862C1B19B400FFE5AC /* ItemRouterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF5D852C1B19B300FFE5AC /* ItemRouterDelegate.swift */; }; 18 | 9BA0B40B28CF20FA00B3F215 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9BA0B40A28CF20FA00B3F215 /* README.md */; }; 19 | C021768726F313B5004149AE /* MVI_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C021768626F313B5004149AE /* MVI_SwiftUIApp.swift */; }; 20 | C021768B26F313B7004149AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C021768A26F313B7004149AE /* Assets.xcassets */; }; 21 | C021768E26F313B7004149AE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C021768D26F313B7004149AE /* Preview Assets.xcassets */; }; 22 | C02176AE26F314D8004149AE /* ListIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176A626F314D8004149AE /* ListIntent.swift */; }; 23 | C02176B026F314D8004149AE /* ListIntentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176A826F314D8004149AE /* ListIntentProtocol.swift */; }; 24 | C02176B126F314D8004149AE /* ListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176A926F314D8004149AE /* ListRouter.swift */; }; 25 | C02176B326F314D8004149AE /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176AB26F314D8004149AE /* ListView.swift */; }; 26 | C02176B426F314D8004149AE /* ListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176AC26F314D8004149AE /* ListModel.swift */; }; 27 | C02176B526F314D8004149AE /* ListModelPotocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176AD26F314D8004149AE /* ListModelPotocols.swift */; }; 28 | C02176C026F315A8004149AE /* MVIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176BF26F315A8004149AE /* MVIContainer.swift */; }; 29 | C02176C426F36FAA004149AE /* WWDCUrlService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176C326F36FAA004149AE /* WWDCUrlService.swift */; }; 30 | C02176C726F370D8004149AE /* SwiftUIData.plist in Resources */ = {isa = PBXBuildFile; fileRef = C02176C626F370D8004149AE /* SwiftUIData.plist */; }; 31 | C02176C926F3748F004149AE /* WWDCUrlServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176C826F3748F004149AE /* WWDCUrlServiceProtocol.swift */; }; 32 | C02176CE26F3771C004149AE /* ListUrlContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176CD26F3768E004149AE /* ListUrlContentView.swift */; }; 33 | C02176D826F38380004149AE /* ItemIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D026F38380004149AE /* ItemIntent.swift */; }; 34 | C02176DA26F38380004149AE /* ItemIntentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D226F38380004149AE /* ItemIntentProtocol.swift */; }; 35 | C02176DB26F38380004149AE /* ItemRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D326F38380004149AE /* ItemRouter.swift */; }; 36 | C02176DD26F38380004149AE /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D526F38380004149AE /* ItemView.swift */; }; 37 | C02176DE26F38380004149AE /* ItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D626F38380004149AE /* ItemModel.swift */; }; 38 | C02176DF26F38380004149AE /* ItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02176D726F38380004149AE /* ItemModelProtocol.swift */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 13DF5D702C1AD18800FFE5AC /* ListModelError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListModelError.swift; sourceTree = ""; }; 43 | 13DF5D732C1AD1D600FFE5AC /* ListExternalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListExternalData.swift; sourceTree = ""; }; 44 | 13DF5D772C1AD45E00FFE5AC /* LoadingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingModifier.swift; sourceTree = ""; }; 45 | 13DF5D7A2C1AD72B00FFE5AC /* View+LoadingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+LoadingModifier.swift"; sourceTree = ""; }; 46 | 13DF5D7D2C1AE53100FFE5AC /* ItemExternalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemExternalData.swift; sourceTree = ""; }; 47 | 13DF5D832C1B174F00FFE5AC /* ListRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRouterDelegate.swift; sourceTree = ""; }; 48 | 13DF5D852C1B19B300FFE5AC /* ItemRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemRouterDelegate.swift; sourceTree = ""; }; 49 | 9BA0B40A28CF20FA00B3F215 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 50 | C021768326F313B5004149AE /* MVI-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MVI-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | C021768626F313B5004149AE /* MVI_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVI_SwiftUIApp.swift; sourceTree = ""; }; 52 | C021768A26F313B7004149AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | C021768D26F313B7004149AE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 54 | C021768F26F313B7004149AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | C02176A626F314D8004149AE /* ListIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListIntent.swift; sourceTree = ""; }; 56 | C02176A826F314D8004149AE /* ListIntentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListIntentProtocol.swift; sourceTree = ""; }; 57 | C02176A926F314D8004149AE /* ListRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRouter.swift; sourceTree = ""; }; 58 | C02176AB26F314D8004149AE /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; 59 | C02176AC26F314D8004149AE /* ListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListModel.swift; sourceTree = ""; }; 60 | C02176AD26F314D8004149AE /* ListModelPotocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListModelPotocols.swift; sourceTree = ""; }; 61 | C02176BF26F315A8004149AE /* MVIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVIContainer.swift; sourceTree = ""; }; 62 | C02176C326F36FAA004149AE /* WWDCUrlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WWDCUrlService.swift; sourceTree = ""; }; 63 | C02176C626F370D8004149AE /* SwiftUIData.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SwiftUIData.plist; sourceTree = ""; }; 64 | C02176C826F3748F004149AE /* WWDCUrlServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WWDCUrlServiceProtocol.swift; sourceTree = ""; }; 65 | C02176CD26F3768E004149AE /* ListUrlContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListUrlContentView.swift; sourceTree = ""; }; 66 | C02176D026F38380004149AE /* ItemIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIntent.swift; sourceTree = ""; }; 67 | C02176D226F38380004149AE /* ItemIntentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIntentProtocol.swift; sourceTree = ""; }; 68 | C02176D326F38380004149AE /* ItemRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemRouter.swift; sourceTree = ""; }; 69 | C02176D526F38380004149AE /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 70 | C02176D626F38380004149AE /* ItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemModel.swift; sourceTree = ""; }; 71 | C02176D726F38380004149AE /* ItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemModelProtocol.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | C021768026F313B5004149AE /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | 13446D2E2AD1308F0081E7F3 /* RouterModifier in Frameworks */, 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | /* End PBXFrameworksBuildPhase section */ 84 | 85 | /* Begin PBXGroup section */ 86 | 135121392ACD9FCA00580A3C /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | ); 90 | name = Frameworks; 91 | sourceTree = ""; 92 | }; 93 | 13DF5D6F2C1AD0DB00FFE5AC /* DataModels */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 13DF5D702C1AD18800FFE5AC /* ListModelError.swift */, 97 | ); 98 | path = DataModels; 99 | sourceTree = ""; 100 | }; 101 | 13DF5D722C1AD1A600FFE5AC /* DataModels */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 13DF5D732C1AD1D600FFE5AC /* ListExternalData.swift */, 105 | ); 106 | path = DataModels; 107 | sourceTree = ""; 108 | }; 109 | 13DF5D752C1AD2DC00FFE5AC /* DesingSystem */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 13DF5D762C1AD44C00FFE5AC /* Modifiers */, 113 | ); 114 | path = DesingSystem; 115 | sourceTree = ""; 116 | }; 117 | 13DF5D762C1AD44C00FFE5AC /* Modifiers */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 13DF5D792C1AD71600FFE5AC /* Loading */, 121 | ); 122 | path = Modifiers; 123 | sourceTree = ""; 124 | }; 125 | 13DF5D792C1AD71600FFE5AC /* Loading */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 13DF5D7A2C1AD72B00FFE5AC /* View+LoadingModifier.swift */, 129 | 13DF5D772C1AD45E00FFE5AC /* LoadingModifier.swift */, 130 | ); 131 | path = Loading; 132 | sourceTree = ""; 133 | }; 134 | 13DF5D7C2C1AE52500FFE5AC /* DataModels */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 13DF5D7D2C1AE53100FFE5AC /* ItemExternalData.swift */, 138 | ); 139 | path = DataModels; 140 | sourceTree = ""; 141 | }; 142 | 13DF5D872C1B1A5500FFE5AC /* Core */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 9B82B41428CD9A2000622105 /* Utilites */, 146 | C02176C126F36F93004149AE /* Services */, 147 | ); 148 | path = Core; 149 | sourceTree = ""; 150 | }; 151 | 9B82B41428CD9A2000622105 /* Utilites */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | C02176BF26F315A8004149AE /* MVIContainer.swift */, 155 | ); 156 | path = Utilites; 157 | sourceTree = ""; 158 | }; 159 | C021767A26F313B5004149AE = { 160 | isa = PBXGroup; 161 | children = ( 162 | 9BA0B40A28CF20FA00B3F215 /* README.md */, 163 | C021768526F313B5004149AE /* MVI-SwiftUI */, 164 | C021768426F313B5004149AE /* Products */, 165 | 135121392ACD9FCA00580A3C /* Frameworks */, 166 | ); 167 | sourceTree = ""; 168 | }; 169 | C021768426F313B5004149AE /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | C021768326F313B5004149AE /* MVI-SwiftUI.app */, 173 | ); 174 | name = Products; 175 | sourceTree = ""; 176 | }; 177 | C021768526F313B5004149AE /* MVI-SwiftUI */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | C021768F26F313B7004149AE /* Info.plist */, 181 | C021768626F313B5004149AE /* MVI_SwiftUIApp.swift */, 182 | C021768A26F313B7004149AE /* Assets.xcassets */, 183 | C021769526F31403004149AE /* Screens */, 184 | 13DF5D752C1AD2DC00FFE5AC /* DesingSystem */, 185 | 13DF5D872C1B1A5500FFE5AC /* Core */, 186 | C021768C26F313B7004149AE /* Preview Content */, 187 | ); 188 | path = "MVI-SwiftUI"; 189 | sourceTree = ""; 190 | }; 191 | C021768C26F313B7004149AE /* Preview Content */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | C021768D26F313B7004149AE /* Preview Assets.xcassets */, 195 | ); 196 | path = "Preview Content"; 197 | sourceTree = ""; 198 | }; 199 | C021769526F31403004149AE /* Screens */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | C02176A526F3145F004149AE /* List */, 203 | C02176CF26F38303004149AE /* Item */, 204 | ); 205 | path = Screens; 206 | sourceTree = ""; 207 | }; 208 | C02176A526F3145F004149AE /* List */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | C02176B826F314EB004149AE /* Model */, 212 | C02176B726F314E6004149AE /* View */, 213 | C02176B626F314E0004149AE /* Intent */, 214 | C02176B926F31515004149AE /* Router */, 215 | ); 216 | path = List; 217 | sourceTree = ""; 218 | }; 219 | C02176B626F314E0004149AE /* Intent */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | C02176A826F314D8004149AE /* ListIntentProtocol.swift */, 223 | C02176A626F314D8004149AE /* ListIntent.swift */, 224 | 13DF5D722C1AD1A600FFE5AC /* DataModels */, 225 | ); 226 | path = Intent; 227 | sourceTree = ""; 228 | }; 229 | C02176B726F314E6004149AE /* View */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | C02176AB26F314D8004149AE /* ListView.swift */, 233 | C02176CC26F3766E004149AE /* UIElements */, 234 | ); 235 | path = View; 236 | sourceTree = ""; 237 | }; 238 | C02176B826F314EB004149AE /* Model */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | C02176AD26F314D8004149AE /* ListModelPotocols.swift */, 242 | C02176AC26F314D8004149AE /* ListModel.swift */, 243 | 13DF5D6F2C1AD0DB00FFE5AC /* DataModels */, 244 | ); 245 | path = Model; 246 | sourceTree = ""; 247 | }; 248 | C02176B926F31515004149AE /* Router */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 13DF5D832C1B174F00FFE5AC /* ListRouterDelegate.swift */, 252 | C02176A926F314D8004149AE /* ListRouter.swift */, 253 | ); 254 | path = Router; 255 | sourceTree = ""; 256 | }; 257 | C02176C126F36F93004149AE /* Services */ = { 258 | isa = PBXGroup; 259 | children = ( 260 | C02176C226F36F9F004149AE /* WWDCUrl */, 261 | ); 262 | path = Services; 263 | sourceTree = ""; 264 | }; 265 | C02176C226F36F9F004149AE /* WWDCUrl */ = { 266 | isa = PBXGroup; 267 | children = ( 268 | C02176C826F3748F004149AE /* WWDCUrlServiceProtocol.swift */, 269 | C02176C326F36FAA004149AE /* WWDCUrlService.swift */, 270 | C02176C526F370AE004149AE /* Data */, 271 | ); 272 | path = WWDCUrl; 273 | sourceTree = ""; 274 | }; 275 | C02176C526F370AE004149AE /* Data */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | C02176C626F370D8004149AE /* SwiftUIData.plist */, 279 | ); 280 | path = Data; 281 | sourceTree = ""; 282 | }; 283 | C02176CC26F3766E004149AE /* UIElements */ = { 284 | isa = PBXGroup; 285 | children = ( 286 | C02176CD26F3768E004149AE /* ListUrlContentView.swift */, 287 | ); 288 | path = UIElements; 289 | sourceTree = ""; 290 | }; 291 | C02176CF26F38303004149AE /* Item */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | C02176E026F38385004149AE /* Model */, 295 | C02176E326F38396004149AE /* View */, 296 | C02176E226F38392004149AE /* Intent */, 297 | C02176E126F3838C004149AE /* Router */, 298 | ); 299 | path = Item; 300 | sourceTree = ""; 301 | }; 302 | C02176E026F38385004149AE /* Model */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | C02176D726F38380004149AE /* ItemModelProtocol.swift */, 306 | C02176D626F38380004149AE /* ItemModel.swift */, 307 | ); 308 | path = Model; 309 | sourceTree = ""; 310 | }; 311 | C02176E126F3838C004149AE /* Router */ = { 312 | isa = PBXGroup; 313 | children = ( 314 | 13DF5D852C1B19B300FFE5AC /* ItemRouterDelegate.swift */, 315 | C02176D326F38380004149AE /* ItemRouter.swift */, 316 | ); 317 | path = Router; 318 | sourceTree = ""; 319 | }; 320 | C02176E226F38392004149AE /* Intent */ = { 321 | isa = PBXGroup; 322 | children = ( 323 | C02176D226F38380004149AE /* ItemIntentProtocol.swift */, 324 | C02176D026F38380004149AE /* ItemIntent.swift */, 325 | 13DF5D7C2C1AE52500FFE5AC /* DataModels */, 326 | ); 327 | path = Intent; 328 | sourceTree = ""; 329 | }; 330 | C02176E326F38396004149AE /* View */ = { 331 | isa = PBXGroup; 332 | children = ( 333 | C02176D526F38380004149AE /* ItemView.swift */, 334 | ); 335 | path = View; 336 | sourceTree = ""; 337 | }; 338 | /* End PBXGroup section */ 339 | 340 | /* Begin PBXNativeTarget section */ 341 | C021768226F313B5004149AE /* MVI-SwiftUI */ = { 342 | isa = PBXNativeTarget; 343 | buildConfigurationList = C021769226F313B7004149AE /* Build configuration list for PBXNativeTarget "MVI-SwiftUI" */; 344 | buildPhases = ( 345 | C021767F26F313B5004149AE /* Sources */, 346 | C021768026F313B5004149AE /* Frameworks */, 347 | C021768126F313B5004149AE /* Resources */, 348 | ); 349 | buildRules = ( 350 | ); 351 | dependencies = ( 352 | ); 353 | name = "MVI-SwiftUI"; 354 | packageProductDependencies = ( 355 | 13446D2D2AD1308F0081E7F3 /* RouterModifier */, 356 | ); 357 | productName = "MVI-SwiftUI"; 358 | productReference = C021768326F313B5004149AE /* MVI-SwiftUI.app */; 359 | productType = "com.apple.product-type.application"; 360 | }; 361 | /* End PBXNativeTarget section */ 362 | 363 | /* Begin PBXProject section */ 364 | C021767B26F313B5004149AE /* Project object */ = { 365 | isa = PBXProject; 366 | attributes = { 367 | BuildIndependentTargetsInParallel = YES; 368 | LastSwiftUpdateCheck = 1250; 369 | LastUpgradeCheck = 1500; 370 | TargetAttributes = { 371 | C021768226F313B5004149AE = { 372 | CreatedOnToolsVersion = 12.5.1; 373 | }; 374 | }; 375 | }; 376 | buildConfigurationList = C021767E26F313B5004149AE /* Build configuration list for PBXProject "MVI-SwiftUI" */; 377 | compatibilityVersion = "Xcode 9.3"; 378 | developmentRegion = en; 379 | hasScannedForEncodings = 0; 380 | knownRegions = ( 381 | en, 382 | Base, 383 | ); 384 | mainGroup = C021767A26F313B5004149AE; 385 | productRefGroup = C021768426F313B5004149AE /* Products */; 386 | projectDirPath = ""; 387 | projectRoot = ""; 388 | targets = ( 389 | C021768226F313B5004149AE /* MVI-SwiftUI */, 390 | ); 391 | }; 392 | /* End PBXProject section */ 393 | 394 | /* Begin PBXResourcesBuildPhase section */ 395 | C021768126F313B5004149AE /* Resources */ = { 396 | isa = PBXResourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | C021768E26F313B7004149AE /* Preview Assets.xcassets in Resources */, 400 | C021768B26F313B7004149AE /* Assets.xcassets in Resources */, 401 | 9BA0B40B28CF20FA00B3F215 /* README.md in Resources */, 402 | C02176C726F370D8004149AE /* SwiftUIData.plist in Resources */, 403 | ); 404 | runOnlyForDeploymentPostprocessing = 0; 405 | }; 406 | /* End PBXResourcesBuildPhase section */ 407 | 408 | /* Begin PBXSourcesBuildPhase section */ 409 | C021767F26F313B5004149AE /* Sources */ = { 410 | isa = PBXSourcesBuildPhase; 411 | buildActionMask = 2147483647; 412 | files = ( 413 | C02176B026F314D8004149AE /* ListIntentProtocol.swift in Sources */, 414 | C02176DD26F38380004149AE /* ItemView.swift in Sources */, 415 | C02176AE26F314D8004149AE /* ListIntent.swift in Sources */, 416 | C02176DA26F38380004149AE /* ItemIntentProtocol.swift in Sources */, 417 | 13DF5D842C1B174F00FFE5AC /* ListRouterDelegate.swift in Sources */, 418 | C02176B326F314D8004149AE /* ListView.swift in Sources */, 419 | 13DF5D712C1AD18800FFE5AC /* ListModelError.swift in Sources */, 420 | C02176C426F36FAA004149AE /* WWDCUrlService.swift in Sources */, 421 | C02176C026F315A8004149AE /* MVIContainer.swift in Sources */, 422 | 13DF5D742C1AD1D600FFE5AC /* ListExternalData.swift in Sources */, 423 | C02176B426F314D8004149AE /* ListModel.swift in Sources */, 424 | C02176B526F314D8004149AE /* ListModelPotocols.swift in Sources */, 425 | C02176DE26F38380004149AE /* ItemModel.swift in Sources */, 426 | C02176DB26F38380004149AE /* ItemRouter.swift in Sources */, 427 | 13DF5D782C1AD45E00FFE5AC /* LoadingModifier.swift in Sources */, 428 | C02176B126F314D8004149AE /* ListRouter.swift in Sources */, 429 | 13DF5D7B2C1AD72B00FFE5AC /* View+LoadingModifier.swift in Sources */, 430 | C02176DF26F38380004149AE /* ItemModelProtocol.swift in Sources */, 431 | C02176CE26F3771C004149AE /* ListUrlContentView.swift in Sources */, 432 | C02176C926F3748F004149AE /* WWDCUrlServiceProtocol.swift in Sources */, 433 | 13DF5D7E2C1AE53100FFE5AC /* ItemExternalData.swift in Sources */, 434 | C021768726F313B5004149AE /* MVI_SwiftUIApp.swift in Sources */, 435 | 13DF5D862C1B19B400FFE5AC /* ItemRouterDelegate.swift in Sources */, 436 | C02176D826F38380004149AE /* ItemIntent.swift in Sources */, 437 | ); 438 | runOnlyForDeploymentPostprocessing = 0; 439 | }; 440 | /* End PBXSourcesBuildPhase section */ 441 | 442 | /* Begin XCBuildConfiguration section */ 443 | C021769026F313B7004149AE /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ALWAYS_SEARCH_USER_PATHS = NO; 447 | CLANG_ANALYZER_NONNULL = YES; 448 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 449 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 450 | CLANG_CXX_LIBRARY = "libc++"; 451 | CLANG_ENABLE_MODULES = YES; 452 | CLANG_ENABLE_OBJC_ARC = YES; 453 | CLANG_ENABLE_OBJC_WEAK = YES; 454 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 455 | CLANG_WARN_BOOL_CONVERSION = YES; 456 | CLANG_WARN_COMMA = YES; 457 | CLANG_WARN_CONSTANT_CONVERSION = YES; 458 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 459 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 460 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 461 | CLANG_WARN_EMPTY_BODY = YES; 462 | CLANG_WARN_ENUM_CONVERSION = YES; 463 | CLANG_WARN_INFINITE_RECURSION = YES; 464 | CLANG_WARN_INT_CONVERSION = YES; 465 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 466 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 467 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 468 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 469 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 470 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 471 | CLANG_WARN_STRICT_PROTOTYPES = YES; 472 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 473 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 474 | CLANG_WARN_UNREACHABLE_CODE = YES; 475 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 476 | COPY_PHASE_STRIP = NO; 477 | DEBUG_INFORMATION_FORMAT = dwarf; 478 | ENABLE_STRICT_OBJC_MSGSEND = YES; 479 | ENABLE_TESTABILITY = YES; 480 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 481 | GCC_C_LANGUAGE_STANDARD = gnu11; 482 | GCC_DYNAMIC_NO_PIC = NO; 483 | GCC_NO_COMMON_BLOCKS = YES; 484 | GCC_OPTIMIZATION_LEVEL = 0; 485 | GCC_PREPROCESSOR_DEFINITIONS = ( 486 | "DEBUG=1", 487 | "$(inherited)", 488 | ); 489 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 490 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 491 | GCC_WARN_UNDECLARED_SELECTOR = YES; 492 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 493 | GCC_WARN_UNUSED_FUNCTION = YES; 494 | GCC_WARN_UNUSED_VARIABLE = YES; 495 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 496 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 497 | MTL_FAST_MATH = YES; 498 | ONLY_ACTIVE_ARCH = YES; 499 | SDKROOT = iphoneos; 500 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 501 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 502 | }; 503 | name = Debug; 504 | }; 505 | C021769126F313B7004149AE /* Release */ = { 506 | isa = XCBuildConfiguration; 507 | buildSettings = { 508 | ALWAYS_SEARCH_USER_PATHS = NO; 509 | CLANG_ANALYZER_NONNULL = YES; 510 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 511 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 512 | CLANG_CXX_LIBRARY = "libc++"; 513 | CLANG_ENABLE_MODULES = YES; 514 | CLANG_ENABLE_OBJC_ARC = YES; 515 | CLANG_ENABLE_OBJC_WEAK = YES; 516 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 517 | CLANG_WARN_BOOL_CONVERSION = YES; 518 | CLANG_WARN_COMMA = YES; 519 | CLANG_WARN_CONSTANT_CONVERSION = YES; 520 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 521 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 522 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 523 | CLANG_WARN_EMPTY_BODY = YES; 524 | CLANG_WARN_ENUM_CONVERSION = YES; 525 | CLANG_WARN_INFINITE_RECURSION = YES; 526 | CLANG_WARN_INT_CONVERSION = YES; 527 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 528 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 529 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 530 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 531 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 532 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 533 | CLANG_WARN_STRICT_PROTOTYPES = YES; 534 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 535 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 536 | CLANG_WARN_UNREACHABLE_CODE = YES; 537 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 538 | COPY_PHASE_STRIP = NO; 539 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 540 | ENABLE_NS_ASSERTIONS = NO; 541 | ENABLE_STRICT_OBJC_MSGSEND = YES; 542 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 543 | GCC_C_LANGUAGE_STANDARD = gnu11; 544 | GCC_NO_COMMON_BLOCKS = YES; 545 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 546 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 547 | GCC_WARN_UNDECLARED_SELECTOR = YES; 548 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 549 | GCC_WARN_UNUSED_FUNCTION = YES; 550 | GCC_WARN_UNUSED_VARIABLE = YES; 551 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 552 | MTL_ENABLE_DEBUG_INFO = NO; 553 | MTL_FAST_MATH = YES; 554 | SDKROOT = iphoneos; 555 | SWIFT_COMPILATION_MODE = wholemodule; 556 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 557 | VALIDATE_PRODUCT = YES; 558 | }; 559 | name = Release; 560 | }; 561 | C021769326F313B7004149AE /* Debug */ = { 562 | isa = XCBuildConfiguration; 563 | buildSettings = { 564 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 565 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 566 | CODE_SIGN_STYLE = Automatic; 567 | DEVELOPMENT_ASSET_PATHS = "\"MVI-SwiftUI/Preview Content\""; 568 | DEVELOPMENT_TEAM = SB522NQPTN; 569 | ENABLE_PREVIEWS = YES; 570 | INFOPLIST_FILE = "MVI-SwiftUI/Info.plist"; 571 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 572 | LD_RUNPATH_SEARCH_PATHS = ( 573 | "$(inherited)", 574 | "@executable_path/Frameworks", 575 | ); 576 | PRODUCT_BUNDLE_IDENTIFIER = "VAnsimov.MVI-SwiftUI"; 577 | PRODUCT_NAME = "$(TARGET_NAME)"; 578 | SWIFT_VERSION = 5.0; 579 | TARGETED_DEVICE_FAMILY = "1,2"; 580 | }; 581 | name = Debug; 582 | }; 583 | C021769426F313B7004149AE /* Release */ = { 584 | isa = XCBuildConfiguration; 585 | buildSettings = { 586 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 587 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 588 | CODE_SIGN_STYLE = Automatic; 589 | DEVELOPMENT_ASSET_PATHS = "\"MVI-SwiftUI/Preview Content\""; 590 | DEVELOPMENT_TEAM = SB522NQPTN; 591 | ENABLE_PREVIEWS = YES; 592 | INFOPLIST_FILE = "MVI-SwiftUI/Info.plist"; 593 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 594 | LD_RUNPATH_SEARCH_PATHS = ( 595 | "$(inherited)", 596 | "@executable_path/Frameworks", 597 | ); 598 | PRODUCT_BUNDLE_IDENTIFIER = "VAnsimov.MVI-SwiftUI"; 599 | PRODUCT_NAME = "$(TARGET_NAME)"; 600 | SWIFT_VERSION = 5.0; 601 | TARGETED_DEVICE_FAMILY = "1,2"; 602 | }; 603 | name = Release; 604 | }; 605 | /* End XCBuildConfiguration section */ 606 | 607 | /* Begin XCConfigurationList section */ 608 | C021767E26F313B5004149AE /* Build configuration list for PBXProject "MVI-SwiftUI" */ = { 609 | isa = XCConfigurationList; 610 | buildConfigurations = ( 611 | C021769026F313B7004149AE /* Debug */, 612 | C021769126F313B7004149AE /* Release */, 613 | ); 614 | defaultConfigurationIsVisible = 0; 615 | defaultConfigurationName = Release; 616 | }; 617 | C021769226F313B7004149AE /* Build configuration list for PBXNativeTarget "MVI-SwiftUI" */ = { 618 | isa = XCConfigurationList; 619 | buildConfigurations = ( 620 | C021769326F313B7004149AE /* Debug */, 621 | C021769426F313B7004149AE /* Release */, 622 | ); 623 | defaultConfigurationIsVisible = 0; 624 | defaultConfigurationName = Release; 625 | }; 626 | /* End XCConfigurationList section */ 627 | 628 | /* Begin XCSwiftPackageProductDependency section */ 629 | 13446D2D2AD1308F0081E7F3 /* RouterModifier */ = { 630 | isa = XCSwiftPackageProductDependency; 631 | productName = RouterModifier; 632 | }; 633 | /* End XCSwiftPackageProductDependency section */ 634 | }; 635 | rootObject = C021767B26F313B5004149AE /* Project object */; 636 | } 637 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/vansimov.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/vansimov.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/vyacheslavansimov.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/MVI-SwiftUI.xcodeproj/project.xcworkspace/xcuserdata/vyacheslavansimov.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/vansimov.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/vansimov.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MVI-SwiftUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/viacheslav.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/viacheslav.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MVI-SwiftUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/vyacheslavansimov.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcodeproj/xcuserdata/vyacheslavansimov.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MVI-SwiftUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVI-SwiftUI.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/MVI-SwiftUI.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MVI-SwiftUI.xcworkspace/xcuserdata/viacheslav.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI/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 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Core/Services/WWDCUrl/Data/SwiftUIData.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Direct and reflect focus in SwiftUI 6 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10023/5/ED227AE3-34ED-45F7-BB9D-7E4F06876C3B/downloads/wwdc2021-10023_hd.mp4?dl=1 7 | What's new in SwiftUI (2020) 8 | https://devstreaming-cdn.apple.com/videos/wwdc/2020/10041/7/85DB087C-0A27-4779-B73A-7C5C888A7C82/wwdc2020_10041_hd.mp4?dl=1 9 | SF Symbols in SwiftUI 10 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10349/4/5C81F023-9887-405D-AF78-7FBD8FACEDEF/downloads/wwdc2021-10349_hd.mp4?dl=1 11 | Localize your SwiftUI app 12 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10220/6/3866585A-3920-44B4-AB3F-03A446FCDE3A/downloads/wwdc2021-10220_hd.mp4?dl=1 13 | What's new in SwiftUI (2021) 14 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10018/4/C1412BB4-40EE-418F-BCFD-09796128093C/downloads/wwdc2021-10018_hd.mp4?dl=1 15 | The SwiftUI cookbook for navigation 16 | https://devstreaming-cdn.apple.com/videos/wwdc/2022/10054/4/E85249AE-F795-40DC-BD9E-A3E385906FE6/downloads/wwdc2022-10054_hd.mp4?dl=1 17 | Use SwiftUI with UIKit 18 | https://devstreaming-cdn.apple.com/videos/wwdc/2022/10072/4/03036EB8-1A2E-4ADD-A5A3-C50A9AFA841C/downloads/wwdc2022-10072_hd.mp4?dl=1 19 | What's new in SwiftUI (2022) 20 | https://devstreaming-cdn.apple.com/videos/wwdc/2022/10052/5/241B4005-877E-40CD-91AA-4CE0714BB2E6/downloads/wwdc2022-10052_hd.mp4?dl=1 21 | Discover concurrency in SwiftUI 22 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10019/6/97B7FCAB-AC78-4A0D-8F28-C5C7AE8C339C/downloads/wwdc2021-10019_hd.mp4?dl=1 23 | Demystify SwiftUI 24 | https://devstreaming-cdn.apple.com/videos/wwdc/2021/10022/7/72A67717-944A-4D86-BFDD-D1B307C722EC/downloads/wwdc2021-10022_hd.mp4?dl=1 25 | 26 | 27 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Core/Services/WWDCUrl/WWDCUrlService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCUrlService.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Foundation 9 | 10 | class WWDCUrlService {} 11 | 12 | // MARK: - Public 13 | 14 | extension WWDCUrlService: WWDCUrlServiceProtocol { 15 | 16 | func fetch(contnet: WWDCUrlType, completion: (Result<[WWDCUrlContent], WWDCUrlError>) -> Void) { 17 | let plist = getPlist(withName: contnet.plistName) 18 | 19 | let contents: [WWDCUrlContent] = plist?.compactMap { 20 | guard let strUrl = $1 as? String, 21 | let url = URL(string: strUrl) 22 | else { return nil } 23 | 24 | return WWDCUrlContent(title: $0, url: url) 25 | } ?? [] 26 | 27 | if contents.isEmpty { 28 | completion(.failure(.emptyData)) 29 | } else { 30 | completion(.success(contents)) 31 | } 32 | } 33 | } 34 | 35 | // MARK: - Private 36 | 37 | private extension WWDCUrlService { 38 | 39 | func getPlist(withName name: String) -> [String: Any]? { 40 | guard let url = Bundle.main.url(forResource: name, withExtension: "plist"), 41 | let data = try? Data(contentsOf:url), 42 | let propertyList = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) 43 | else { return nil } 44 | 45 | return propertyList as? [String: Any] 46 | } 47 | } 48 | 49 | private extension WWDCUrlType { 50 | var plistName: String { 51 | switch self { 52 | case .swiftUI: return "SwiftUIData" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Core/Services/WWDCUrl/WWDCUrlServiceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCUrlServiceProtocol.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol WWDCUrlServiceProtocol { 11 | 12 | func fetch(contnet: WWDCUrlType, completion: (Result<[WWDCUrlContent], WWDCUrlError>) -> Void) 13 | } 14 | 15 | enum WWDCUrlType { 16 | 17 | case swiftUI 18 | } 19 | 20 | enum WWDCUrlError: Error { 21 | 22 | case emptyData 23 | } 24 | 25 | struct WWDCUrlContent { 26 | 27 | var id: String { title + url.absoluteString } 28 | let title: String 29 | let url: URL 30 | } 31 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Core/Utilites/MVIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVIContainer.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class MVIContainer: ObservableObject { 12 | 13 | // MARK: Public 14 | 15 | let intent: Intent 16 | let model: Model 17 | 18 | // MARK: private 19 | 20 | private var cancellable: Set = [] 21 | 22 | // MARK: Life cycle 23 | 24 | init(intent: Intent, model: Model, modelChangePublisher: ObjectWillChangePublisher) { 25 | self.intent = intent 26 | self.model = model 27 | 28 | modelChangePublisher 29 | .receive(on: RunLoop.main) 30 | .sink(receiveValue: objectWillChange.send) 31 | .store(in: &cancellable) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MVI-SwiftUI/DesingSystem/Modifiers/Loading/LoadingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingModifier.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Status 11 | 12 | enum LoadingStatus { 13 | 14 | case fullScreen 15 | case square 16 | case inactive 17 | } 18 | 19 | 20 | // MARK: - Modifier 21 | 22 | struct LoadingModifier { 23 | 24 | let status: LoadingStatus 25 | let loadingText: String 26 | 27 | init( 28 | status: LoadingStatus, 29 | loadingText: String 30 | ) { 31 | self.status = status 32 | self.loadingText = loadingText 33 | } 34 | } 35 | 36 | // MARK: - ViewModifier 37 | 38 | extension LoadingModifier: ViewModifier { 39 | 40 | func body(content: Content) -> some View { 41 | content 42 | .overlay { 43 | switch status { 44 | case .fullScreen: 45 | fullScreenView 46 | .transition(.opacity) 47 | .ignoresSafeArea() 48 | 49 | case .square: 50 | squareView 51 | .transition(.opacity) 52 | 53 | case .inactive: 54 | EmptyView() 55 | } 56 | } 57 | .disabled({ 58 | switch status { 59 | case .inactive, .fullScreen: 60 | return false 61 | 62 | case .square: 63 | return true 64 | } 65 | }()) 66 | } 67 | } 68 | 69 | // MARK: - Views 70 | 71 | private extension LoadingModifier { 72 | 73 | var fullScreenView: some View { 74 | ZStack { 75 | Color.white 76 | ProgressView { 77 | if loadingText.isEmpty { 78 | EmptyView() 79 | } else { 80 | Text(loadingText) 81 | } 82 | } 83 | .tint(.gray) 84 | } 85 | } 86 | 87 | var squareView: some View { 88 | VStack { 89 | Spacer() 90 | 91 | ZStack { 92 | Color.white 93 | .frame(width: 130, height: 130) 94 | .clipShape(RoundedRectangle(cornerRadius: 25.0)) 95 | .overlay { 96 | RoundedRectangle(cornerRadius: 25.0) 97 | .stroke(Color(white: 1, opacity: 0.15), lineWidth: 1.0) 98 | } 99 | .shadow(color: Color(white: 0, opacity: 0.3), radius: 20) 100 | ProgressView { 101 | if loadingText.isEmpty { 102 | EmptyView() 103 | } else { 104 | Text(loadingText) 105 | .frame(width: 110) 106 | .lineLimit(1) 107 | .minimumScaleFactor(0.5) 108 | } 109 | } 110 | .tint(.gray) 111 | } 112 | Spacer() 113 | } 114 | } 115 | } 116 | 117 | #if DEBUG 118 | // MARK: - #Preview 119 | 120 | private struct PreviewView: View { 121 | 122 | var body: some View { 123 | VStack { 124 | Color(white: 0.9) 125 | Color(white: 0.8) 126 | Color(white: 0.7) 127 | Color(white: 0.9) 128 | Color(white: 0.8) 129 | Color(white: 0.7) 130 | } 131 | .loading(status: .square, loadingText: "Loading") 132 | } 133 | } 134 | 135 | #Preview { PreviewView() } 136 | #endif 137 | 138 | -------------------------------------------------------------------------------- /MVI-SwiftUI/DesingSystem/Modifiers/Loading/View+LoadingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+LoadingModifier.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoadingConfiguration { 11 | 12 | var status: LoadingStatus 13 | var loadingText: String? 14 | } 15 | 16 | extension View { 17 | 18 | func loading( 19 | status: LoadingStatus, 20 | loadingText: String = "Loading..." 21 | ) -> some View { 22 | modifier(LoadingModifier(status: status, loadingText: loadingText)) 23 | } 24 | 25 | func loading(configuration: LoadingConfiguration) -> some View { 26 | modifier(LoadingModifier( 27 | status: configuration.status, 28 | loadingText: configuration.loadingText ?? "Loading..." 29 | )) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /MVI-SwiftUI/MVI_SwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVI_SwiftUIApp.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MVI_SwiftUIApp: App { 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ListView( 16 | data: ListExternalData(), 17 | urlService: WWDCUrlService() 18 | ) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Intent/DataModels/ItemExternalData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemExternalData.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ItemExternalData { 11 | 12 | let title: String 13 | let url: URL 14 | } 15 | 16 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Intent/ItemIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemIntent.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class ItemIntent { 11 | 12 | // Model 13 | private weak var model: ItemModelActionsProtocol? 14 | 15 | // Dependencies 16 | // ... 17 | 18 | // Business Data 19 | private let externalData: ItemExternalData 20 | 21 | // MARK: Life cycle 22 | 23 | init( 24 | model: ItemModelActionsProtocol, 25 | externalData: ItemExternalData 26 | ) { 27 | self.externalData = externalData 28 | self.model = model 29 | 30 | self.model?.displayDefaultContent(externalData: self.externalData) 31 | } 32 | } 33 | 34 | // MARK: - Public 35 | 36 | extension ItemIntent: ItemIntentProtocol { 37 | 38 | func viewOnAppear() { 39 | model?.displayLoading(status: .fullScreen) 40 | model?.videoPlayerPlay() 41 | model?.displayLoading(status: .inactive) 42 | } 43 | 44 | func viewOnDisappear() { 45 | model?.videoPlayerStop() 46 | } 47 | 48 | func didTapPlaying() { 49 | model?.togglePlaing() 50 | } 51 | 52 | func onMuteToggleTap(newValue: Bool) { 53 | model?.displayMuteToggle(value: newValue) 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Intent/ItemIntentProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemIntentProtocol.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | protocol ItemIntentProtocol { 9 | 10 | func viewOnAppear() 11 | func viewOnDisappear() 12 | func didTapPlaying() 13 | func onMuteToggleTap(newValue: Bool) 14 | } 15 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Model/ItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemModel.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import AVKit 11 | 12 | final class ItemModel: ObservableObject, ItemModelStatePotocol { 13 | 14 | @Published var navigationTitle: String = "" 15 | @Published var playingText: String = "play" 16 | @Published var player: AVPlayer = AVPlayer(playerItem: nil) 17 | @Published var isSoundMute: Bool = false 18 | @Published var loadingConfiguration: LoadingConfiguration = LoadingConfiguration(status: .inactive) 19 | 20 | let routerEvents = ItemRouter.RouterEventsType() 21 | } 22 | 23 | // MARK: - Actions Protocol 24 | 25 | extension ItemModel: ItemModelActionsProtocol { 26 | 27 | func displayDefaultContent(externalData: ItemExternalData) { 28 | loadingConfiguration = LoadingConfiguration(status: .fullScreen) 29 | navigationTitle = externalData.title 30 | playingText = "play" 31 | isSoundMute = false 32 | player = AVPlayer(playerItem: nil) 33 | player.replaceCurrentItem(with: AVPlayerItem(url: externalData.url)) 34 | } 35 | 36 | func videoPlayerPlay() { 37 | player.play() 38 | changePlaingText(timeControlStatus: player.timeControlStatus) 39 | 40 | } 41 | func videoPlayerStop() { 42 | player.pause() 43 | } 44 | 45 | func togglePlaing() { 46 | switch player.timeControlStatus { 47 | case .paused: 48 | player.play() 49 | 50 | case .waitingToPlayAtSpecifiedRate, .playing: 51 | player.pause() 52 | 53 | @unknown default: 54 | break 55 | } 56 | changePlaingText(timeControlStatus: player.timeControlStatus) 57 | } 58 | 59 | 60 | func displayMuteToggle(value: Bool) { 61 | isSoundMute = value 62 | player.isMuted = isSoundMute 63 | } 64 | 65 | func displayLoading(status: LoadingStatus) { 66 | loadingConfiguration.status = status 67 | } 68 | } 69 | 70 | // MARK: - Actions - Route 71 | 72 | extension ItemModel { 73 | 74 | func dismiss() { 75 | routerEvents.dismiss() 76 | } 77 | } 78 | 79 | // MARK: - Private 80 | 81 | private extension ItemModel { 82 | 83 | func changePlaingText(timeControlStatus: AVPlayer.TimeControlStatus) { 84 | switch timeControlStatus { 85 | case .paused: 86 | playingText = "play" 87 | 88 | case .waitingToPlayAtSpecifiedRate, .playing: 89 | playingText = "pause" 90 | 91 | @unknown default: 92 | playingText = "" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Model/ItemModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemModelActionsProtocol.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import AVKit 9 | 10 | // MARK: - View State 11 | 12 | protocol ItemModelStatePotocol { 13 | 14 | var navigationTitle: String { get } 15 | var playingText: String { get } 16 | var player: AVPlayer { get } 17 | var isSoundMute: Bool { get } 18 | var loadingConfiguration: LoadingConfiguration { get } 19 | var routerEvents: ItemRouter.RouterEventsType { get } 20 | } 21 | 22 | // MARK: - Intent Actions 23 | 24 | protocol ItemModelActionsProtocol: AnyObject { 25 | 26 | // Display Content 27 | func displayDefaultContent(externalData: ItemExternalData) 28 | func videoPlayerPlay() 29 | func videoPlayerStop() 30 | func togglePlaing() 31 | func displayMuteToggle(value: Bool) 32 | func displayLoading(status: LoadingStatus) 33 | 34 | // Route 35 | func dismiss() 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Router/ItemRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemRouter.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import RouterModifier 10 | 11 | struct ItemRouter: RouterModifierProtocol { 12 | 13 | typealias RouterEventsType = RouterEvents 14 | 15 | let routerEvents: RouterEventsType 16 | weak var delegate: ItemRouterDelegate? 17 | } 18 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/Router/ItemRouterDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemRouterDelegate.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ItemRouterDelegate: AnyObject {} 11 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/Item/View/ItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemView.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import AVKit 10 | 11 | struct ItemView { 12 | 13 | @StateObject var container: MVIContainer 14 | 15 | private var intent: ItemIntentProtocol { container.intent } 16 | private var state: ItemModelStatePotocol { container.model } 17 | 18 | init( 19 | data: ItemExternalData 20 | ) { 21 | let model = ItemModel() 22 | let intent = ItemIntent( 23 | model: model, 24 | externalData: data 25 | ) 26 | let container = MVIContainer( 27 | intent: intent as ItemIntentProtocol, 28 | model: model as ItemModelStatePotocol, 29 | modelChangePublisher: model.objectWillChange 30 | ) 31 | self._container = StateObject(wrappedValue: container) 32 | } 33 | } 34 | 35 | // MARK: - Body 36 | 37 | extension ItemView: View { 38 | 39 | var body: some View { 40 | bodyView() 41 | .onAppear(perform: intent.viewOnAppear) 42 | .navigationBarTitle(state.navigationTitle, displayMode: .inline) 43 | .loading(configuration: state.loadingConfiguration) 44 | .modifier(ItemRouter( 45 | routerEvents: state.routerEvents, 46 | delegate: intent as? ItemRouterDelegate 47 | )) 48 | .onDisappear(perform: intent.viewOnDisappear) 49 | } 50 | } 51 | 52 | // MARK: - Views 53 | 54 | private extension ItemView { 55 | 56 | func bodyView() -> some View { 57 | VStack { 58 | VideoPlayer(player: state.player) 59 | .cornerRadius(8) 60 | 61 | Toggle( 62 | "Mute sound", 63 | isOn: Binding( 64 | get: { 65 | state.isSoundMute 66 | }, 67 | set: { newValue in 68 | intent.onMuteToggleTap(newValue: newValue) 69 | }) 70 | ).padding() 71 | 72 | Button { 73 | self.intent.didTapPlaying() 74 | } label: { 75 | Text(state.playingText) 76 | .foregroundColor(.white) 77 | .padding(.vertical, 12) 78 | .padding(.horizontal, 36) 79 | .background { 80 | RoundedRectangle(cornerRadius: 25.0) 81 | } 82 | } 83 | } 84 | .padding() 85 | } 86 | } 87 | 88 | #if DEBUG 89 | // MARK: - #Preview 90 | 91 | private struct PreviewView: View { 92 | 93 | let url: URL! = URL(string: "https://devstreaming-cdn.apple.com/videos/wwdc/2021/10019/6/97B7FCAB-AC78-4A0D-8F28-C5C7AE8C339C/downloads/wwdc2021-10019_hd.mp4?dl=1") 94 | 95 | var body: some View { 96 | ItemView( 97 | data: ItemExternalData( 98 | title: "Discover concurrency in SwiftUI", 99 | url: url 100 | ) 101 | ) 102 | } 103 | } 104 | 105 | #Preview { PreviewView() } 106 | #endif 107 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Intent/DataModels/ListExternalData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListExternalData.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | struct ListExternalData {} 9 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Intent/ListIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListIntent.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class ListIntent { 11 | 12 | // Model 13 | private weak var model: ListModelActionsProtocol? 14 | 15 | // Dependencies 16 | private let urlService: WWDCUrlServiceProtocol 17 | 18 | // Business Data 19 | private let externalData: ListExternalData 20 | private var contents: [WWDCUrlContent] = [] 21 | 22 | // MARK: Life cycle 23 | 24 | init( 25 | model: ListModelActionsProtocol, 26 | externalData: ListExternalData, 27 | urlService: WWDCUrlServiceProtocol 28 | ) { 29 | self.urlService = urlService 30 | 31 | self.externalData = externalData 32 | self.model = model 33 | 34 | self.model?.displayDefaultContent(externalData: self.externalData) 35 | } 36 | } 37 | 38 | // MARK: - Public 39 | 40 | extension ListIntent: ListIntentProtocol { 41 | 42 | func viewOnAppear() { 43 | model?.displayLoading(status: .fullScreen) 44 | 45 | urlService.fetch(contnet: .swiftUI) { [weak self] result in 46 | guard let self else { return } 47 | 48 | switch result { 49 | case let .success(contents): 50 | self.contents = contents 51 | self.model?.dispalyContent(urlContents: contents) 52 | 53 | case let .failure(error): 54 | self.model?.dispalyError(error: error) 55 | } 56 | 57 | self.model?.displayLoading(status: .inactive) 58 | } 59 | } 60 | 61 | func onTapUrlContent(id: String) { 62 | guard let content = contents.first(where: { $0.id == id }) else { return } 63 | 64 | model?.routeTo(screen: .videoPlayer( 65 | title: content.title, 66 | url: content.url 67 | )) 68 | } 69 | } 70 | 71 | // MARK: - ListRouterDelegate 72 | 73 | extension ListIntent: ListRouterDelegate { 74 | 75 | func onScreenDismiss(type: ListRouterScreenType) {} 76 | } 77 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Intent/ListIntentProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListIntentProtocol.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | protocol ListIntentProtocol { 9 | 10 | func viewOnAppear() 11 | func onTapUrlContent(id: String) 12 | } 13 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Model/DataModels/ListModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListModelError.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | enum ListModelScreenError: Error { 9 | 10 | case error(text: String) 11 | } 12 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Model/ListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListModel.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Model 11 | 12 | final class ListModel: ObservableObject, ListModelStatePotocol { 13 | 14 | @Published var navigationTitle = "" 15 | @Published var urlContents: [ListUrlContentState] = [] 16 | @Published var loadingConfiguration: LoadingConfiguration = LoadingConfiguration(status: .inactive) 17 | @Published var screenError: ListModelScreenError? = nil 18 | 19 | let routerEvents = ListRouter.RouterEventsType() 20 | } 21 | 22 | 23 | // MARK: - Actions - Display Content 24 | 25 | extension ListModel: ListModelActionsProtocol { 26 | 27 | func displayDefaultContent(externalData: ListExternalData) { 28 | navigationTitle = "Swift UI Videos" 29 | urlContents = [] 30 | loadingConfiguration = LoadingConfiguration(status: .fullScreen) 31 | screenError = nil 32 | } 33 | 34 | func dispalyContent(urlContents contents: [WWDCUrlContent]) { 35 | urlContents = contents 36 | .map { ListUrlContentState(id: $0.id, title: $0.title) } 37 | .sorted(by: { $0.title < $1.title }) 38 | screenError = nil 39 | } 40 | 41 | func dispalyError(error: Error) { 42 | screenError = .error(text: "Fail") 43 | } 44 | 45 | func displayLoading(status: LoadingStatus) { 46 | loadingConfiguration.status = status 47 | } 48 | } 49 | 50 | // MARK: - Actions - Route 51 | 52 | extension ListModel { 53 | 54 | func routeTo(screen: ListRouterScreenType) { 55 | routerEvents.routeTo(screen) 56 | } 57 | 58 | func displayAlert(alert: ListRouterAlertType) { 59 | routerEvents.presentAlert(alert) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Model/ListModelPotocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListModelStatePotocol.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - State 11 | 12 | protocol ListModelStatePotocol { 13 | 14 | var navigationTitle: String { get } 15 | var urlContents: [ListUrlContentState] { get } 16 | var loadingConfiguration: LoadingConfiguration { get } 17 | var screenError: ListModelScreenError? { get } 18 | 19 | var routerEvents: ListRouter.RouterEventsType { get } 20 | } 21 | 22 | // MARK: - Actions 23 | 24 | protocol ListModelActionsProtocol: AnyObject { 25 | 26 | // Display Content 27 | func displayDefaultContent(externalData: ListExternalData) 28 | func dispalyContent(urlContents: [WWDCUrlContent]) 29 | func dispalyError(error: Error) 30 | func displayLoading(status: LoadingStatus) 31 | 32 | // Route 33 | func routeTo(screen: ListRouterScreenType) 34 | func displayAlert(alert: ListRouterAlertType) 35 | } 36 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Router/ListRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListRouter.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import RouterModifier 10 | 11 | struct ListRouter: RouterModifierProtocol { 12 | 13 | typealias RouterEventsType = RouterEvents 14 | 15 | let routerEvents: RouterEventsType 16 | weak var delegate: ListRouterDelegate? 17 | } 18 | 19 | // MARK: - Screens 20 | 21 | enum ListRouterScreenType: RouterScreenProtocol { 22 | 23 | case videoPlayer(title: String, url: URL) 24 | } 25 | 26 | extension ListRouter { 27 | 28 | @ViewBuilder 29 | func getScreen(for type: ListRouterScreenType) -> some View { 30 | switch type { 31 | case let .videoPlayer(title, url): 32 | ItemView(data: ItemExternalData(title: title, url: url)) 33 | } 34 | } 35 | 36 | func getScreenPresentationType(for type: ListRouterScreenType) -> RouterScreenPresentationType { 37 | switch type { 38 | case .videoPlayer: 39 | return .navigationLink 40 | } 41 | } 42 | 43 | func onScreenDismiss(type: ListRouterScreenType) { 44 | delegate?.onScreenDismiss(type: type) 45 | } 46 | } 47 | 48 | 49 | // MARK: - Alerts 50 | 51 | enum ListRouterAlertType: RouterAlertScreenProtocol { 52 | 53 | case error(title: String, message: String) 54 | } 55 | 56 | extension ListRouter { 57 | 58 | @ViewBuilder 59 | func getAlertTitle(for type: ListRouterAlertType) -> Text? { 60 | switch type { 61 | case let .error(title, _): 62 | Text(title) 63 | } 64 | } 65 | 66 | @ViewBuilder 67 | func getAlertMessage(for type: ListRouterAlertType) -> some View { 68 | switch type { 69 | case let .error(_, message): 70 | Text(message) 71 | } 72 | } 73 | 74 | @ViewBuilder 75 | func getAlertActions(for type: ListRouterAlertType) -> some View { 76 | Button("OK", role: .cancel, action: {}) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/Router/ListRouterDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListRouterDelegate.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ListRouterDelegate: AnyObject { 11 | 12 | func onScreenDismiss(type: ListRouterScreenType) 13 | } 14 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/View/Custom Elements/ListUrlContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListUrlContentView.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - State 11 | 12 | struct ListUrlContentState: Hashable, Identifiable { 13 | 14 | let id: String 15 | let title: String 16 | } 17 | 18 | // MARK: - View 19 | 20 | struct ListUrlContentView { 21 | 22 | @State var state: ListUrlContentState 23 | 24 | var didTap: (_ id: String) -> Void 25 | } 26 | 27 | // MARK: - Body 28 | 29 | extension ListUrlContentView: View { 30 | 31 | var body: some View { 32 | content 33 | } 34 | } 35 | 36 | // MARK: - Views 37 | 38 | private extension ListUrlContentView { 39 | 40 | var content: some View { 41 | Button( 42 | action: { 43 | didTap(state.id) 44 | }, 45 | label: { 46 | Text(state.title) 47 | .foregroundColor(.black) 48 | .frame(maxWidth: .infinity) 49 | .padding() 50 | .background(backgroundView) 51 | } 52 | ) 53 | } 54 | 55 | var backgroundView: some View { 56 | Color( 57 | .sRGB, 58 | red: 250/255, 59 | green: 250/255, 60 | blue: 254/255, 61 | opacity: 1 62 | ) 63 | .cornerRadius(14) 64 | .shadow( 65 | color: Color(.sRGB, white: 0, opacity: 0.15), 66 | radius: 4, 67 | x: 1, 68 | y: 1 69 | ) 70 | } 71 | } 72 | 73 | #if DEBUG 74 | // MARK: - #Preview 75 | 76 | private struct PreviewView: View { 77 | 78 | var body: some View { 79 | VStack { 80 | ListUrlContentView( 81 | state: ListUrlContentState( 82 | id: UUID().uuidString, 83 | title: "Demystify SwiftUI"), 84 | didTap: { _ in } 85 | ) 86 | 87 | ListUrlContentView( 88 | state: ListUrlContentState( 89 | id: UUID().uuidString, 90 | title: "Localize your SwiftUI app"), 91 | didTap: { _ in } 92 | ) 93 | } 94 | .padding() 95 | } 96 | } 97 | 98 | #Preview { PreviewView() } 99 | #endif 100 | -------------------------------------------------------------------------------- /MVI-SwiftUI/Screens/List/View/ListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListView.swift 3 | // MVI-SwiftUI 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ListView { 11 | 12 | @StateObject var container: MVIContainer 13 | 14 | private var intent: ListIntentProtocol { container.intent } 15 | private var state: ListModelStatePotocol { container.model } 16 | 17 | init( 18 | data: ListExternalData, 19 | urlService: WWDCUrlServiceProtocol 20 | ) { 21 | let model = ListModel() 22 | let intent = ListIntent( 23 | model: model, 24 | externalData: data, 25 | urlService: urlService 26 | ) 27 | let container = MVIContainer( 28 | intent: intent as ListIntentProtocol, 29 | model: model as ListModelStatePotocol, 30 | modelChangePublisher: model.objectWillChange 31 | ) 32 | self._container = StateObject(wrappedValue: container) 33 | } 34 | } 35 | 36 | // MARK: - Body 37 | 38 | extension ListView: View { 39 | 40 | var body: some View { 41 | NavigationView { 42 | content 43 | .navigationTitle(state.navigationTitle) 44 | .loading(configuration: state.loadingConfiguration) 45 | .onAppear(perform: { 46 | intent.viewOnAppear() 47 | }) 48 | .modifier(ListRouter( 49 | routerEvents: state.routerEvents, 50 | delegate: intent as? ListRouterDelegate 51 | )) 52 | } 53 | } 54 | } 55 | 56 | // MARK: - Views 57 | 58 | private extension ListView { 59 | 60 | var content: some View { 61 | ScrollView { 62 | listItemsView 63 | .padding(.vertical) 64 | } 65 | .overlay { 66 | errorView 67 | } 68 | } 69 | 70 | // ListItems View 71 | var listItemsView: some View { 72 | LazyVStack(spacing: 16) { 73 | ForEach(state.urlContents) { content in 74 | ListUrlContentView(state: content, didTap: { 75 | intent.onTapUrlContent(id: $0) 76 | }) 77 | .padding(.horizontal) 78 | } 79 | } 80 | } 81 | 82 | // Error View 83 | @ViewBuilder 84 | var errorView: some View { 85 | state.screenError.map { error in 86 | switch error { 87 | case let .error(text): 88 | ZStack { 89 | Color.white 90 | Text(text) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | #if DEBUG 98 | // MARK: - #Preview 99 | 100 | private struct PreviewView: View { 101 | 102 | var body: some View { 103 | ListView( 104 | data: ListExternalData(), 105 | urlService: WWDCUrlService() 106 | ) 107 | } 108 | } 109 | 110 | #Preview { PreviewView() } 111 | #endif 112 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/.swiftpm/xcode/package.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/MyLibraries/RouterModifier/.swiftpm/xcode/package.xcworkspace/xcuserdata/viacheslav.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/.swiftpm/xcode/xcuserdata/viacheslav.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RouterModifier-Package.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 14 11 | 12 | RouterModifier.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | RouterModifierTests.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 7 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "RouterModifier", 8 | platforms: [.macOS(.v12), .iOS(.v15)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "RouterModifier", 13 | targets: ["RouterModifier"]), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package, defining a module or a test suite. 17 | // Targets can depend on other targets in this package and products from dependencies. 18 | .target( 19 | name: "RouterModifier"), 20 | .testTarget( 21 | name: "RouterModifierTests", 22 | dependencies: ["RouterModifier"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/README.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | #### Publication: [medium.com/@vyacheslavansimov/swiftui-and-mvi-3acac8d4416a](https://medium.com/@vyacheslavansimov/swiftui-and-mvi-3acac8d4416a) 4 | 5 | 6 | ## How to use Router? 7 | 8 | ### Implementation Router 9 | 10 | Below is the most complete version, if you don't need something, you don't have to write it. 11 | 12 | **Step 1**: Create a `enum` for the list of screens the View will open to. It should implement the `RouterScreenProtocol` protocol. 13 | 14 | ```swift 15 | enum SomeRouterScreenType: RouterScreenProtocol { 16 | 17 | case productScreen(id: UUID) 18 | } 19 | ``` 20 | 21 | **Step 2**: Create a `enum` for the list of alerts that the View will display. It should implement the `RouterAlertScreenProtocol` protocol. 22 | 23 | ```swift 24 | enum SomeRouterAlertType: RouterAlertScreenProtocol { 25 | 26 | case error(title: String, message: String) 27 | } 28 | ``` 29 | 30 | **Step 3**: We need to implement RouterModifierProtocol is ViewModifier in your router. 31 | 32 | ```swift 33 | struct SomeRouter: RouterModifierProtocol { 34 | 35 | // If you don't need Alerts, you can use `RouterDefaultAlert`. Example: RouterEvents 36 | // If you do not need to go to other screens, then use `RouterEmptyScreen`. Example: RouterEvents 37 | let routerEvents: RouterEvents 38 | } 39 | ``` 40 | 41 | **Step 4**: Implement the functions getScreenPresentationType(for:), getScreen(for:), onScreenDismiss(type:) in your router 42 | 43 | ```swift 44 | extension SomeRouter { 45 | 46 | // Optional 47 | func getScreenPresentationType(for type: SomeRouterScreenType) -> RouterScreenPresentationType { 48 | .fullScreenCover 49 | } 50 | 51 | // Optional 52 | @ViewBuilder 53 | func getScreen(for type: SomeRouterScreenType) -> some View { 54 | switch type { 55 | case let .productScreen(id): 56 | Text("Product Screen View: \(id.uuidString)") 57 | } 58 | } 59 | 60 | // Optional 61 | func onScreenDismiss(type: SomeRouterScreenType) {} 62 | } 63 | ``` 64 | 65 | **Step 5**: Implement the functions getAlertTitle(for:), getAlertMessage(for:), getAlertActions(for:) in your router 66 | 67 | ```swift 68 | extension SomeRouter { 69 | 70 | // Optional 71 | func getAlertTitle(for type: SomeRouterAlertType) -> Text? { 72 | switch type { 73 | case let .error(title, _): 74 | Text(title) 75 | } 76 | } 77 | 78 | // Optional 79 | @ViewBuilder 80 | func geteAlertMessage(for type: SomeRouterAlertType) -> some View { 81 | switch type { 82 | case let .error(_, message): 83 | Text(message) 84 | } 85 | } 86 | 87 | // Optional 88 | @ViewBuilder 89 | func getAlertActions(for type: SomeRouterAlertType) -> some View { 90 | Button("Yes", role: .none, action: { 91 | ... 92 | }) 93 | Button("Cancel", role: .cancel, action: {}) 94 | } 95 | } 96 | ``` 97 | 98 | ### Use Router 99 | 100 | How do I use the router? You can see this in the following example: 101 | 102 | 103 | ```swift 104 | struct SomeView: View { 105 | 106 | let routerEvents = RouterEvents() 107 | 108 | var body: some View { 109 | Text("Hello, World!") 110 | .modifier(SomeRouter(routerEvents: routerEvents)) 111 | .onAppear { 112 | routerEvents.routeTo(.group(id: UUID())) 113 | } 114 | } 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/Modifier/RouterAlertModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterAlertModifier.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | public protocol RouterAlertScreenProtocol {} 12 | 13 | struct RouterAlertModifier 14 | where Actions: View, Message: View, ScreenType: RouterAlertScreenProtocol { 15 | 16 | // MARK: Public 17 | 18 | let publisher: AnyPublisher 19 | let title: (ScreenType) -> Text? 20 | let message: (ScreenType) -> Message? 21 | let actions: (ScreenType) -> Actions? 22 | 23 | 24 | // MARK: Private 25 | 26 | @State 27 | private var screenType: ScreenType? 28 | private var isPresented: Binding { 29 | Binding( 30 | get: { screenType != nil }, 31 | set: { if !$0 { screenType = nil } } 32 | ) 33 | } 34 | 35 | private var titleText: Text { 36 | guard let screenType else { return Text("") } 37 | return title(screenType) ?? Text("") 38 | } 39 | 40 | @ViewBuilder 41 | private var messageView: some View { 42 | if let type = screenType, let messageView = message(type) { 43 | messageView 44 | } else { 45 | EmptyView() 46 | } 47 | } 48 | } 49 | 50 | // MARK: - ViewModifier 51 | 52 | extension RouterAlertModifier: ViewModifier { 53 | 54 | func body(content: Content) -> some View { 55 | content 56 | .alert( 57 | titleText, 58 | isPresented: isPresented, 59 | actions: { 60 | if let type = screenType, let actionsView = actions(type) { 61 | actionsView 62 | } else { 63 | EmptyView() 64 | } 65 | }, 66 | message: { messageView } 67 | ) 68 | .onReceive(publisher) { screenType = $0 } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/Modifier/RouterDismissModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterDismissModifier.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct RouterDismissModifier: ViewModifier { 12 | 13 | // MARK: Public 14 | 15 | let publisher: AnyPublisher 16 | 17 | // MARK: Private 18 | 19 | @Environment(\.presentationMode) 20 | private var presentationMode 21 | 22 | // MARK: Life cycle 23 | 24 | func body(content: Content) -> some View { 25 | content 26 | .onReceive(publisher) { _ in 27 | presentationMode.wrappedValue.dismiss() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/Modifier/RouterNavigationDestinationModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterNavigationDestinationModifier.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct RouterNavigationDestinationModifier 12 | where ScreenView: View, ScreenType: RouterScreenProtocol { 13 | 14 | // MARK: Public 15 | 16 | let publisher: AnyPublisher 17 | var screen: (ScreenType) -> ScreenView 18 | let onDismiss: ((ScreenType) -> Void)? 19 | 20 | // MARK: Private 21 | 22 | @State 23 | private var screenType: ScreenType? 24 | private var isPresented: Binding { 25 | Binding( 26 | get: { screenType != nil }, 27 | set: { 28 | if !$0 { 29 | if let type = screenType { onDismiss?(type) } 30 | screenType = nil 31 | } 32 | } 33 | ) 34 | } 35 | } 36 | 37 | // MARK: - ViewModifier 38 | 39 | extension RouterNavigationDestinationModifier: ViewModifier { 40 | 41 | func body(content: Content) -> some View { 42 | if #available(iOS 16.0, *, macOS 13.0, *) { 43 | content 44 | .navigationDestination( 45 | isPresented: isPresented, 46 | destination: { 47 | if let type = screenType { 48 | screen(type) 49 | } else { 50 | EmptyView() 51 | } 52 | } 53 | ) 54 | .onReceive(publisher) { screenType = $0 } 55 | } else { 56 | content 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/Modifier/RouterNavigationLinkModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterNavigationLinkModifier.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | 12 | struct RouterNavigationLinkModifier 13 | where ScreenView: View, ScreenType: RouterScreenProtocol { 14 | 15 | // MARK: Public 16 | 17 | let publisher: AnyPublisher 18 | var screen: (ScreenType) -> ScreenView 19 | let onDismiss: ((ScreenType) -> Void)? 20 | 21 | // MARK: Private 22 | 23 | @State 24 | private var screenType: ScreenType? 25 | private var isActive: Binding { 26 | Binding( 27 | get: { screenType != nil }, 28 | set: { 29 | if !$0 { 30 | if let type = screenType { onDismiss?(type) } 31 | screenType = nil 32 | } 33 | } 34 | ) 35 | } 36 | } 37 | 38 | // MARK: - ViewModifier 39 | 40 | extension RouterNavigationLinkModifier: ViewModifier { 41 | 42 | func body(content: Content) -> some View { 43 | ZStack { 44 | NavigationLink( 45 | "", 46 | isActive: isActive, 47 | destination: { 48 | if let type = screenType { 49 | screen(type) 50 | } else { 51 | EmptyView() 52 | } 53 | } 54 | ) 55 | content 56 | } 57 | .onReceive(publisher) { screenType = $0 } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/Modifier/RouterSheetModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterSheetModifier.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct RouterSheetModifier where ScreenView: View, ScreenType: RouterScreenProtocol { 12 | 13 | // MARK: Public 14 | 15 | var isFullScreenCover: Bool = false 16 | let publisher: AnyPublisher 17 | var screen: (ScreenType) -> ScreenView 18 | let onDismiss: ((ScreenType) -> Void)? 19 | 20 | // MARK: Private 21 | 22 | @State private var screenType: ScreenType? 23 | 24 | private var isPresented: Binding { 25 | Binding( 26 | get: { screenType != nil }, 27 | set: { 28 | if !$0 { 29 | if let type = screenType { onDismiss?(type) } 30 | screenType = nil 31 | } 32 | }) 33 | } 34 | 35 | @ViewBuilder 36 | private var screenContent: some View { 37 | if let type = screenType { 38 | screen(type) 39 | } else { 40 | EmptyView() 41 | } 42 | } 43 | } 44 | 45 | // MARK: - ViewModifier 46 | 47 | extension RouterSheetModifier: ViewModifier { 48 | 49 | func body(content: Content) -> some View { 50 | #if os(iOS) 51 | if isFullScreenCover { 52 | content 53 | .onReceive(publisher) { screenType = $0 } 54 | .fullScreenCover( 55 | isPresented: isPresented, 56 | content: { screenContent } 57 | ) 58 | } else { 59 | content 60 | .onReceive(publisher) { screenType = $0 } 61 | .sheet( 62 | isPresented: isPresented, 63 | content: { screenContent } 64 | ) 65 | } 66 | #else 67 | content 68 | .onReceive(publisher) { screenType = $0 } 69 | .sheet( 70 | isPresented: isPresented, 71 | content: { screenContent }) 72 | #endif 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/Core/RouterModifierProtocol+Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterModifierProtocol+Body.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension RouterModifierProtocol { 11 | 12 | public func body(content: Content) -> some View { 13 | content 14 | .modifier(navigationDestinationModifier) 15 | .modifier(navigationLinkModifier) 16 | .modifier(sheetModifier) 17 | .modifier(fullScreenCoverModifier) 18 | .modifier(alertModifier) 19 | .modifier(dismissModifier) 20 | } 21 | } 22 | 23 | // MARK: - Modifiers 24 | 25 | private extension RouterModifierProtocol { 26 | 27 | var navigationDestinationModifier: some ViewModifier { 28 | ConditionalModifier( 29 | isEmpty: routerEvents.screenIsEmpty, 30 | viewModifier: { 31 | RouterNavigationDestinationModifier( 32 | publisher: routerEvents.screenSubject 33 | .filter { 34 | if #available(iOS 16.0, *, macOS 13.0, *) { 35 | return getScreenPresentationType(for: $0) == .navigationDestination 36 | } else { 37 | return false 38 | } 39 | } 40 | .receive(on: RunLoop.main) 41 | .eraseToAnyPublisher(), 42 | screen: getScreen, 43 | onDismiss: onScreenDismiss 44 | ) 45 | }) 46 | } 47 | 48 | var navigationLinkModifier: some ViewModifier { 49 | ConditionalModifier( 50 | isEmpty: routerEvents.screenIsEmpty, 51 | viewModifier: { 52 | RouterNavigationLinkModifier( 53 | publisher: routerEvents.screenSubject 54 | .filter { getScreenPresentationType(for: $0) == .navigationLink } 55 | .receive(on: RunLoop.main) 56 | .eraseToAnyPublisher(), 57 | screen: getScreen, 58 | onDismiss: onScreenDismiss 59 | ) 60 | }) 61 | } 62 | 63 | var sheetModifier: some ViewModifier { 64 | ConditionalModifier( 65 | isEmpty: routerEvents.screenIsEmpty, 66 | viewModifier: { 67 | RouterSheetModifier( 68 | isFullScreenCover: false, 69 | publisher: routerEvents.screenSubject 70 | .filter { getScreenPresentationType(for: $0) == .sheet } 71 | .receive(on: RunLoop.main) 72 | .eraseToAnyPublisher(), 73 | screen: getScreen, 74 | onDismiss: onScreenDismiss 75 | ) 76 | }) 77 | } 78 | 79 | var fullScreenCoverModifier: some ViewModifier { 80 | ConditionalModifier( 81 | isEmpty: routerEvents.screenIsEmpty, 82 | viewModifier: { 83 | RouterSheetModifier( 84 | isFullScreenCover: true, 85 | publisher: routerEvents.screenSubject 86 | .filter { getScreenPresentationType(for: $0) == .fullScreenCover } 87 | .receive(on: RunLoop.main) 88 | .eraseToAnyPublisher(), 89 | screen: getScreen, 90 | onDismiss: onScreenDismiss 91 | ) 92 | }) 93 | } 94 | 95 | var alertModifier: some ViewModifier { 96 | RouterAlertModifier( 97 | publisher: routerEvents.alertSubject 98 | .receive(on: RunLoop.main) 99 | .eraseToAnyPublisher(), 100 | title: getAlertTitle, 101 | message: getAlertMessage, 102 | actions: getAlertActions 103 | ) 104 | } 105 | 106 | var dismissModifier: some ViewModifier { 107 | RouterDismissModifier( 108 | publisher: routerEvents.dismissSubject 109 | .receive(on: RunLoop.main) 110 | .eraseToAnyPublisher() 111 | ) 112 | } 113 | } 114 | 115 | // MARK: - Default values 116 | 117 | public extension RouterModifierProtocol { 118 | 119 | func getScreenPresentationType(for type: RouterScreenType) -> RouterScreenPresentationType { .sheet } 120 | 121 | func getScreen(for type: RouterScreenType) -> some View { EmptyView() } 122 | 123 | func onScreenDismiss(type: RouterScreenType) {} 124 | 125 | func getAlertTitle(for type: RouterAlertType) -> Text? { 126 | guard let type = type as? RouterDefaultAlert else { return nil } 127 | 128 | switch type { 129 | case let .defaultAlert(title, _, _): 130 | return title.map { Text($0) } 131 | } 132 | } 133 | 134 | func getAlertMessage(for type: RouterAlertType) -> some View { 135 | (type as? RouterDefaultAlert).map { type in 136 | switch type { 137 | case let .defaultAlert(_, message, _): 138 | return message.map { Text($0) } 139 | } 140 | } 141 | } 142 | 143 | func getAlertActions(for type: RouterAlertType) -> some View { 144 | (type as? RouterDefaultAlert).map { type in 145 | switch type { 146 | case let .defaultAlert(_, _, cancelText): 147 | return Button(role: .cancel, action: {}, label: { Text(cancelText) }) 148 | } 149 | } 150 | } 151 | } 152 | 153 | // MARK: - Helper classes 154 | 155 | private struct ConditionalModifier: ViewModifier where Modifier: ViewModifier { 156 | 157 | var isEmpty: Bool 158 | var viewModifier: () -> Modifier 159 | 160 | func body(content: Content) -> some View { 161 | if isEmpty { 162 | content.modifier(EmptyModifier()) 163 | } else { 164 | content.modifier(viewModifier()) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/HelperClasses/RouterDefaultAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterDefaultAlert.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | public enum RouterDefaultAlert: RouterAlertScreenProtocol { 9 | 10 | case defaultAlert(title: String?, message: String?, cancelText: String) 11 | } 12 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/HelperClasses/RouterEmptyScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterEmptyScreen.swift 3 | // Router 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | public enum RouterEmptyScreen: RouterScreenProtocol {} 9 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/HelperClasses/RouterEvents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterEvents.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Combine 9 | 10 | public struct RouterEvents 11 | where ScreenType: RouterScreenProtocol, AlertType: RouterAlertScreenProtocol { 12 | 13 | let screenSubject = PassthroughSubject() 14 | let alertSubject = PassthroughSubject() 15 | let dismissSubject = PassthroughSubject() 16 | 17 | var screenIsEmpty: Bool { screenSubject is PassthroughSubject } 18 | 19 | public init() {} 20 | 21 | public func routeTo(_ type: ScreenType) { 22 | screenSubject.send(type) 23 | } 24 | 25 | public func presentAlert(_ type: AlertType) { 26 | alertSubject.send(type) 27 | } 28 | 29 | public func dismiss() { 30 | dismissSubject.send() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/HelperClasses/RouterScreenPresentationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterScreenPresentationType.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | public enum RouterScreenPresentationType { 9 | 10 | case sheet 11 | case fullScreenCover 12 | 13 | /// For NavigationStack or NavigationSplitView 14 | @available(iOS 16.0, *, macOS 13.0, *) 15 | case navigationDestination 16 | 17 | /// For NavigationView 18 | @available(iOS, introduced: 13.0, deprecated: 16.0, message: "use .navigationDestination, inside a NavigationStack or NavigationSplitView") 19 | @available(macOS, introduced: 10.15, deprecated: 13.0, message: "use .navigationDestination, inside a NavigationStack or NavigationSplitView") 20 | case navigationLink 21 | } 22 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/HelperClasses/RouterScreenProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterScreenProtocol.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | public protocol RouterScreenProtocol {} 9 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Sources/RouterModifier/RouterModifierProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterModifierProtocol.swift 3 | // RouterModifier 4 | // 5 | // Created by Vyacheslav Ansimov. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | /// RouterModifierProtocol 12 | /// 13 | /// Implementation Router 14 | /// 15 | /// Below is the most complete version, if you don't need something, you don't have to write it. 16 | /// 17 | /// **Step 1**: Create a `enum` for the list of screens the View will open to. It should implement the `RouterScreenProtocol` protocol. 18 | /// 19 | /// ```swift 20 | /// enum SomeRouterScreenType: RouterScreenProtocol { 21 | /// 22 | /// case productScreen(id: UUID) 23 | /// } 24 | /// ``` 25 | /// 26 | /// **Step 2**: Create a `enum` for the list of alerts that the View will display. It should implement the `RouterAlertScreenProtocol` protocol. 27 | /// 28 | /// ```swift 29 | /// enum SomeRouterAlertType: RouterAlertScreenProtocol { 30 | /// 31 | /// case error(title: String, message: String) 32 | /// } 33 | /// ``` 34 | /// 35 | /// **Step 3**: We need to implement RouterModifierProtocol is ViewModifier in your router. 36 | /// 37 | /// ```swift 38 | /// struct SomeRouter: RouterModifierProtocol { 39 | /// 40 | /// // If you don't need Alerts, you can use `RouterDefaultAlert`. Example: RouterEvents 41 | /// // If you do not need to go to other screens, then use `RouterEmptyScreen`. Example: RouterEvents 42 | /// let routerEvents: RouterEvents 43 | /// } 44 | /// ``` 45 | /// 46 | /// **Step 4**: Implement the functions getScreenPresentationType(for:), getScreen(for:), onScreenDismiss(type:) in your router 47 | /// 48 | /// ```swift 49 | /// extension SomeRouter { 50 | /// 51 | /// // Optional 52 | /// func getScreenPresentationType(for type: SomeRouterScreenType) -> RouterScreenPresentationType { 53 | /// .fullScreenCover 54 | /// } 55 | /// 56 | /// // Optional 57 | /// @ViewBuilder 58 | /// func getScreen(for type: SomeRouterScreenType) -> some View { 59 | /// switch type { 60 | /// case let .productScreen(id): 61 | /// Text("Product Screen View: \(id.uuidString)") 62 | /// } 63 | /// } 64 | /// 65 | /// // Optional 66 | /// func onScreenDismiss(type: SomeRouterScreenType) {} 67 | /// } 68 | /// ``` 69 | /// 70 | /// **Step 5**: Implement the functions getAlertTitle(for:), getAlertMessage(for:), getAlertActions(for:) in your router 71 | /// 72 | /// ```swift 73 | /// extension SomeRouter { 74 | /// 75 | /// // Optional 76 | /// func getAlertTitle(for type: SomeRouterAlertType) -> Text? { 77 | /// switch type { 78 | /// case let .error(title, _): 79 | /// Text(title) 80 | /// } 81 | /// } 82 | /// 83 | /// // Optional 84 | /// @ViewBuilder 85 | /// func geteAlertMessage(for type: SomeRouterAlertType) -> some View { 86 | /// switch type { 87 | /// case let .error(_, message): 88 | /// Text(message) 89 | /// } 90 | /// } 91 | /// 92 | /// // Optional 93 | /// @ViewBuilder 94 | /// func getAlertActions(for type: SomeRouterAlertType) -> some View { 95 | /// Button("Yes", role: .none, action: { 96 | /// ... 97 | /// }) 98 | /// Button("Cancel", role: .cancel, action: {}) 99 | /// } 100 | /// } 101 | /// ``` 102 | /// 103 | /// How do I use the router? You can see this in the following example: 104 | /// 105 | /// ```swift 106 | /// struct SomeView: View { 107 | /// 108 | /// let routerEvents = RouterEvents() 109 | /// 110 | /// var body: some View { 111 | /// Text("Hello, World!") 112 | /// .modifier(SomeRouter(routerEvents: routerEvents)) 113 | /// .onAppear { 114 | /// routerEvents.routeTo(.group(id: UUID())) 115 | /// } 116 | /// } 117 | /// } 118 | /// ``` 119 | public protocol RouterModifierProtocol: ViewModifier { 120 | 121 | associatedtype RouterScreenType: RouterScreenProtocol 122 | associatedtype RouterAlertType: RouterAlertScreenProtocol 123 | 124 | associatedtype RouterScreenView: View 125 | associatedtype AlertMessageView: View 126 | associatedtype AlertActionsView: View 127 | 128 | var routerEvents: RouterEvents { get } 129 | 130 | // MARK: Screens 131 | 132 | func getScreen(for type: RouterScreenType) -> RouterScreenView 133 | func getScreenPresentationType(for type: RouterScreenType) -> RouterScreenPresentationType 134 | func onScreenDismiss(type: RouterScreenType) 135 | 136 | // MARK: Alerts 137 | 138 | func getAlertTitle(for type: RouterAlertType) -> Text? 139 | func getAlertMessage(for type: RouterAlertType) -> AlertMessageView 140 | func getAlertActions(for type: RouterAlertType) -> AlertActionsView 141 | } 142 | -------------------------------------------------------------------------------- /MyLibraries/RouterModifier/Tests/RouterModifierTests/RouterModifierTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import RouterModifier 3 | 4 | final class RouterModifierTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI and MVI 2 | 3 | #### Publication: [medium.com/@vyacheslavansimov/swiftui-and-mvi-3acac8d4416a](https://medium.com/@vyacheslavansimov/swiftui-and-mvi-3acac8d4416a) 4 | 5 | 6 | ## MVI — brief history and principle of operation 7 | 8 | This pattern was first described by JavaScript developer Andre Staltz. The general principles can be found [here](https://staltz.com/unidirectional-user-interface-architectures.html) 9 | 10 | ![](README_sources/image_001.jpeg) 11 | 12 | 13 | - Intent: function from Observable of user events to Observable of “actions” 14 | - Model: function from Observable of actions to Observable of state 15 | - View: function from Observable of state to Observable of rendering 16 | Custom element: subsection of the rendering which is in itself a UI program. May be implemented as MVI, or as a Web Component. Is optional to use in a View. 17 | 18 | MVI has a reactive approach. Each module (function) expects some event, and after receiving and processing it, it passes this event to the next module. It turns out an unidirectional flow. 19 | 20 | In the mobile app the diagram looks very close to the original with only minor changes: 21 | 22 | ![](README_sources/image_002.png) 23 | 24 | - Intent receives an event from View and communicates with the business logic 25 | - Model receives data from Intent and prepares it for display. The Model also keeps the current state of the View. 26 | - View displays the prepared data. 27 | 28 | To provide a unidirectional data flow, you need to make sure that the View has a reference to the Intent, the Intent has a reference to the Model, which in turn has a reference to the View. 29 | 30 | The main problem in implementing this approach in SwiftUI is View. View is a structure and Model cannot have references to View. To solve this problem, you can introduce an additional layer Container, which main task is to keep references to Intent and Model, and provide accessibility to the layers so that the unidirectional data flow is truly unidirectional. 31 | It sounds complicated, but it is quite simple in practice. 32 | 33 | ## MVI 34 | 35 | Container is independent of the life cycle of the View because it is @StateObject. Every time the View is reinitialization, Intent and Model remain the same. 36 | 37 | ![](README_sources/image_003.png) 38 | 39 | There is a unidirectional data flow between the modules. 40 | 41 | 1) View receives the user's event. 42 | 2) Intent receives an event from View and communicates with the business logic 43 | 3) Model receives data from Intent and prepares it for display. The Model also keeps the current state of the View. 44 | 4) View displays the prepared data. 45 | 46 | ![](README_sources/image_004.png) 47 | 48 | # Templates for Xcode 49 | 50 | ### xctemplate 51 | 52 | The template can be found in Templates-for-Xcode/xctemplate 53 | 54 | Add the file *.xctemplate to the folder: 55 | /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates 56 | 57 | The template can be found in the Xcode menu 58 | File -> New -> File... 59 | 60 | ### Router 61 | 62 | The router is in Swift Package Manager and can be copied and reused in your projects 63 | 64 | # How to use Router? 65 | 66 | ### Implementation Router 67 | 68 | Below is the most complete version, if you don't need something, you don't have to write it. 69 | 70 | **Step 1**: Create a `enum` for the list of screens the View will open to. It should implement the `RouterScreenProtocol` protocol. 71 | 72 | ```swift 73 | enum SomeRouterScreenType: RouterScreenProtocol { 74 | 75 | case productScreen(id: UUID) 76 | } 77 | ``` 78 | 79 | **Step 2**: Create a `enum` for the list of alerts that the View will display. It should implement the `RouterAlertScreenProtocol` protocol. 80 | 81 | ```swift 82 | enum SomeRouterAlertType: RouterAlertScreenProtocol { 83 | 84 | case error(title: String, message: String) 85 | } 86 | ``` 87 | 88 | **Step 3**: We need to implement RouterModifierProtocol is ViewModifier in your router. 89 | 90 | ```swift 91 | struct SomeRouter: RouterModifierProtocol { 92 | 93 | // If you don't need Alerts, you can use `RouterDefaultAlert`. Example: RouterEvents 94 | // If you do not need to go to other screens, then use `RouterEmptyScreen`. Example: RouterEvents 95 | let routerEvents: RouterEvents 96 | } 97 | ``` 98 | 99 | **Step 4**: Implement the functions getScreenPresentationType(for:), getScreen(for:), onScreenDismiss(type:) in your router 100 | 101 | ```swift 102 | extension SomeRouter { 103 | 104 | // Optional 105 | func getScreenPresentationType(for type: SomeRouterScreenType) -> RouterScreenPresentationType { 106 | .fullScreenCover 107 | } 108 | 109 | // Optional 110 | @ViewBuilder 111 | func getScreen(for type: SomeRouterScreenType) -> some View { 112 | switch type { 113 | case let .productScreen(id): 114 | Text("Product Screen View: \(id.uuidString)") 115 | } 116 | } 117 | 118 | // Optional 119 | func onScreenDismiss(type: SomeRouterScreenType) {} 120 | } 121 | ``` 122 | 123 | **Step 5**: Implement the functions getAlertTitle(for:), getAlertMessage(for:), getAlertActions(for:) in your router 124 | 125 | ```swift 126 | extension SomeRouter { 127 | 128 | // Optional 129 | func getAlertTitle(for type: SomeRouterAlertType) -> Text? { 130 | switch type { 131 | case let .error(title, _): 132 | Text(title) 133 | } 134 | } 135 | 136 | // Optional 137 | @ViewBuilder 138 | func geteAlertMessage(for type: SomeRouterAlertType) -> some View { 139 | switch type { 140 | case let .error(_, message): 141 | Text(message) 142 | } 143 | } 144 | 145 | // Optional 146 | @ViewBuilder 147 | func getAlertActions(for type: SomeRouterAlertType) -> some View { 148 | Button("Yes", role: .none, action: { 149 | ... 150 | }) 151 | Button("Cancel", role: .cancel, action: {}) 152 | } 153 | } 154 | ``` 155 | 156 | ### Use Router 157 | 158 | How do I use the router? You can see this in the following example: 159 | 160 | 161 | ```swift 162 | struct SomeView: View { 163 | 164 | let routerEvents = RouterEvents() 165 | 166 | var body: some View { 167 | Text("Hello, World!") 168 | .modifier(SomeRouter(routerEvents: routerEvents)) 169 | .onAppear { 170 | routerEvents.routeTo(.group(id: UUID())) 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | 177 | # Maintainers 178 | 179 | * [Vyacheslav Ansimov](https://www.linkedin.com/in/vansimov/) 180 | -------------------------------------------------------------------------------- /README_sources/image_001.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/README_sources/image_001.jpeg -------------------------------------------------------------------------------- /README_sources/image_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/README_sources/image_002.png -------------------------------------------------------------------------------- /README_sources/image_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/README_sources/image_003.png -------------------------------------------------------------------------------- /README_sources/image_004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/README_sources/image_004.png -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/TemplateIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/TemplateIcon.pdf -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | MVI cycle files. 9 | Summary 10 | MVI cycle files. 11 | SortOrder 12 | 30 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | File 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | Options 22 | 23 | 24 | Description 25 | The name for MVI cycle 26 | Identifier 27 | sceneName 28 | Name 29 | New MVI cycle name: 30 | NotPersisted 31 | 32 | Required 33 | 34 | Type 35 | text 36 | 37 | 38 | Default 39 | ___VARIABLE_sceneName:identifier___ 40 | Identifier 41 | productName 42 | Type 43 | static 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___ExternalData.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | struct ___VARIABLE_sceneName___ExternalData {} 4 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___Intent.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | class ___VARIABLE_sceneName___Intent { 6 | 7 | // Model 8 | private weak var model: ___VARIABLE_sceneName___ModelActionsProtocol? 9 | 10 | // Dependencies 11 | // ... 12 | 13 | // Busines Data 14 | private let externalData: ___VARIABLE_sceneName___ExternalData 15 | 16 | // MARK: Life cycle 17 | 18 | init( 19 | model: ___VARIABLE_sceneName___ModelActionsProtocol, 20 | externalData: ___VARIABLE_sceneName___ExternalData 21 | ) { 22 | self.externalData = externalData 23 | self.model = model 24 | } 25 | 26 | } 27 | 28 | // MARK: - Public 29 | 30 | extension ___VARIABLE_sceneName___Intent: ___VARIABLE_sceneName___IntentProtocol { 31 | 32 | func viewOnAppear() { 33 | model?.displayLoading() 34 | 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 36 | self.model?.display(content: Int.random(in: 0 ..< 100)) 37 | } 38 | } 39 | 40 | func viewOnDisappear() {} 41 | } 42 | 43 | // MARK: - ___VARIABLE_sceneName___RouterDelegate 44 | 45 | extension ___VARIABLE_sceneName___Intent: ___VARIABLE_sceneName___RouterDelegate { 46 | 47 | func onScreenDismiss(type: ___VARIABLE_sceneName___ScreenType) {} 48 | } 49 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___IntentProtocol.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | protocol ___VARIABLE_sceneName___IntentProtocol { 4 | 5 | func viewOnAppear() 6 | func viewOnDisappear() 7 | } 8 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___Model.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | final class ___VARIABLE_sceneName___Model: ObservableObject, ___VARIABLE_sceneName___ModelStatePotocol { 6 | 7 | @Published var text: String = "" 8 | 9 | let routerEvents = ___VARIABLE_sceneName___Router.RouterEventsType() 10 | } 11 | 12 | // MARK: - Actions Protocol 13 | 14 | extension ___VARIABLE_sceneName___Model: ___VARIABLE_sceneName___ModelActionsProtocol { 15 | 16 | func displayLoading() { 17 | text = "loading" 18 | } 19 | 20 | func display(content: Int) { 21 | text = "That number is " + String(content) 22 | } 23 | 24 | func display(error: Error) { 25 | text = "Error" 26 | } 27 | 28 | func routeTo(screen: ___VARIABLE_sceneName___ScreenType) { 29 | routerEvents.routeTo(screen) 30 | } 31 | 32 | func show(alert: ___VARIABLE_sceneName___AlertType) { 33 | routerEvents.presentAlert(alert) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___ModelProtocol.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | // MARK: - View State 6 | 7 | protocol ___VARIABLE_sceneName___ModelStatePotocol { 8 | var text: String { get set } 9 | var routerEvents: ___VARIABLE_sceneName___Router.RouterEventsType { get } 10 | } 11 | 12 | // MARK: - Intent Actions 13 | 14 | protocol ___VARIABLE_sceneName___ModelActionsProtocol: AnyObject { 15 | func displayLoading() 16 | func display(content: Int) 17 | func display(error: Error) 18 | 19 | func routeTo(screen: ___VARIABLE_sceneName___ScreenType) 20 | func show(alert: ___VARIABLE_sceneName___AlertType) 21 | } 22 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___Router.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | struct ___VARIABLE_sceneName___Router: RouterModifierProtocol { 6 | 7 | // If you don't need Alerts, you can use `RouterDefaultAlert`. Example: RouterEvents 8 | // If you do not need to go to other screens, then use `RouterEmptyScreen`. Example: RouterEvents 9 | typealias RouterEventsType = RouterEvents<___VARIABLE_sceneName___ScreenType, ___VARIABLE_sceneName___AlertType> 10 | 11 | let routerEvents: RouterEventsType 12 | weak var delegate: ___VARIABLE_sceneName___RouterDelegate? 13 | } 14 | 15 | // MARK: - Screens 16 | 17 | enum ___VARIABLE_sceneName___ScreenType: RouterScreenProtocol { 18 | 19 | case sameScreen 20 | } 21 | 22 | extension ___VARIABLE_sceneName___Router { 23 | 24 | func getScreenPresentationType(for type: ___VARIABLE_sceneName___ScreenType) -> RouterScreenPresentationType { 25 | switch type { 26 | case .sameScreen: 27 | return .navigationLink 28 | } 29 | } 30 | 31 | @ViewBuilder 32 | func getScreen(for type: ___VARIABLE_sceneName___ScreenType) -> some View { 33 | switch type { 34 | case .sameScreen: 35 | Text("Same Screen") 36 | } 37 | } 38 | 39 | func onScreenDismiss(type: ___VARIABLE_sceneName___ScreenType) { 40 | delegate?.onScreenDismiss(type: type) 41 | } 42 | } 43 | 44 | 45 | // MARK: - Alerts 46 | 47 | enum ___VARIABLE_sceneName___AlertType: RouterAlertScreenProtocol { 48 | 49 | case error(title: String, message: String) 50 | } 51 | 52 | extension ___VARIABLE_sceneName___Router { 53 | 54 | func getAlertTitle(for type: ___VARIABLE_sceneName___AlertType) -> Text? { 55 | switch type { 56 | case let .error(title, _): 57 | return Text(title) 58 | } 59 | } 60 | 61 | func getAlertMessage(for type: ___VARIABLE_sceneName___AlertType) -> some View { 62 | switch type { 63 | case let .error(_, message): 64 | return Text(message) 65 | } 66 | } 67 | 68 | func getAlertActions(for type: ___VARIABLE_sceneName___AlertType) -> some View { 69 | Button("Cancel", role: .cancel, action: {}) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___RouterDelegate.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | protocol ___VARIABLE_sceneName___RouterDelegate: AnyObject { 6 | 7 | func onScreenDismiss(type: ___VARIABLE_sceneName___ScreenType) 8 | } 9 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI+Router.xctemplate/___FILEBASENAME___View.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | struct ___VARIABLE_sceneName___View { 6 | 7 | @StateObject var container: MVIContainer<___VARIABLE_sceneName___IntentProtocol, ___VARIABLE_sceneName___ModelStatePotocol> 8 | 9 | private var intent: ___VARIABLE_sceneName___IntentProtocol { container.intent } 10 | private var state: ___VARIABLE_sceneName___ModelStatePotocol { container.model } 11 | 12 | init(data: ___VARIABLE_sceneName___ExternalData) { 13 | let model = ___VARIABLE_sceneName___Model() 14 | let intent = ___VARIABLE_sceneName___Intent(model: model, externalData: data) 15 | let container = MVIContainer( 16 | intent: intent as ___VARIABLE_sceneName___IntentProtocol, 17 | model: model as ___VARIABLE_sceneName___ModelStatePotocol, 18 | modelChangePublisher: model.objectWillChange 19 | ) 20 | self._container = StateObject(wrappedValue: container) 21 | } 22 | } 23 | 24 | // MARK: - View 25 | 26 | extension ___VARIABLE_sceneName___View: View { 27 | 28 | var body: some View { 29 | Text(state.text) 30 | .modifier(___VARIABLE_sceneName___Router( 31 | routerEvents: state.routerEvents, 32 | delegate: intent as? ___VARIABLE_sceneName___RouterDelegate 33 | )) 34 | .onAppear(perform: intent.viewOnAppear) 35 | .onDisappear(perform: intent.viewOnDisappear) 36 | } 37 | } 38 | 39 | #if DEBUG 40 | // MARK: - Previews 41 | 42 | #Preview { 43 | ___VARIABLE_sceneName___View(data: ___VARIABLE_sceneName___ExternalData()) 44 | } 45 | #endif 46 | 47 | // MARK: - MVIContainer 48 | 49 | import SwiftUI 50 | import Combine 51 | 52 | final private class MVIContainer: ObservableObject { 53 | 54 | // MARK: Public 55 | 56 | let intent: Intent 57 | let model: Model 58 | 59 | // MARK: private 60 | 61 | private var cancellable: Set = [] 62 | 63 | // MARK: Life cycle 64 | 65 | init(intent: Intent, model: Model, modelChangePublisher: ObjectWillChangePublisher) { 66 | self.intent = intent 67 | self.model = model 68 | 69 | modelChangePublisher 70 | .receive(on: RunLoop.main) 71 | .sink(receiveValue: objectWillChange.send) 72 | .store(in: &cancellable) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/TemplateIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VAnsimov/MVI-SwiftUI/b17180b72be1820adf952832d2af16171e2f8e20/Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/TemplateIcon.pdf -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | MVI cycle files. 9 | Summary 10 | MVI cycle files. 11 | SortOrder 12 | 30 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | File 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | Options 22 | 23 | 24 | Description 25 | The name for MVI cycle 26 | Identifier 27 | sceneName 28 | Name 29 | New MVI cycle name: 30 | NotPersisted 31 | 32 | Required 33 | 34 | Type 35 | text 36 | 37 | 38 | Default 39 | ___VARIABLE_sceneName:identifier___ 40 | Identifier 41 | productName 42 | Type 43 | static 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___ExternalData.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | struct ___VARIABLE_sceneName___ExternalData {} 4 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___Intent.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | class ___VARIABLE_sceneName___Intent { 6 | 7 | // Model 8 | private weak var model: ___VARIABLE_sceneName___ModelActionsProtocol? 9 | 10 | // Dependencies 11 | // ... 12 | 13 | // Busines Data 14 | private let externalData: ___VARIABLE_sceneName___ExternalData 15 | 16 | // MARK: Life cycle 17 | 18 | init( 19 | model: ___VARIABLE_sceneName___ModelActionsProtocol, 20 | externalData: ___VARIABLE_sceneName___ExternalData 21 | ) { 22 | self.externalData = externalData 23 | self.model = model 24 | } 25 | } 26 | 27 | // MARK: - Public 28 | 29 | extension ___VARIABLE_sceneName___Intent: ___VARIABLE_sceneName___IntentProtocol { 30 | 31 | func viewOnAppear() { 32 | model?.displayLoading() 33 | 34 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 35 | self.model?.display(content: Int.random(in: 0 ..< 100)) 36 | } 37 | } 38 | 39 | func viewOnDisappear() {} 40 | } 41 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___IntentProtocol.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | protocol ___VARIABLE_sceneName___IntentProtocol { 4 | 5 | func viewOnAppear() 6 | func viewOnDisappear() 7 | } 8 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___Model.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | final class ___VARIABLE_sceneName___Model: ObservableObject, ___VARIABLE_sceneName___ModelStatePotocol { 6 | 7 | @Published var text: String = "" 8 | } 9 | 10 | // MARK: - Actions Protocol 11 | 12 | extension ___VARIABLE_sceneName___Model: ___VARIABLE_sceneName___ModelActionsProtocol { 13 | 14 | func displayLoading() { 15 | text = "loading" 16 | } 17 | 18 | func display(content: Int) { 19 | text = "That number is " + String(content) 20 | } 21 | 22 | func display(error: Error) { 23 | text = "Error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___ModelProtocol.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | // MARK: - View State 6 | 7 | protocol ___VARIABLE_sceneName___ModelStatePotocol { 8 | 9 | var text: String { get set } 10 | } 11 | 12 | // MARK: - Intent Actions 13 | 14 | protocol ___VARIABLE_sceneName___ModelActionsProtocol: AnyObject { 15 | 16 | func displayLoading() 17 | func display(content: Int) 18 | func display(error: Error) 19 | } 20 | -------------------------------------------------------------------------------- /Templates_for_Xcode/xctemplate/SwiftUI MVI.xctemplate/___FILEBASENAME___View.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | struct ___VARIABLE_sceneName___View { 6 | 7 | @StateObject var container: MVIContainer<___VARIABLE_sceneName___IntentProtocol, ___VARIABLE_sceneName___ModelStatePotocol> 8 | 9 | private var intent: ___VARIABLE_sceneName___IntentProtocol { container.intent } 10 | private var state: ___VARIABLE_sceneName___ModelStatePotocol { container.model } 11 | 12 | init(data: ___VARIABLE_sceneName___ExternalData) { 13 | let model = ___VARIABLE_sceneName___Model() 14 | let intent = ___VARIABLE_sceneName___Intent(model: model, externalData: data) 15 | let container = MVIContainer( 16 | intent: intent as ___VARIABLE_sceneName___IntentProtocol, 17 | model: model as ___VARIABLE_sceneName___ModelStatePotocol, 18 | modelChangePublisher: model.objectWillChange 19 | ) 20 | self._container = StateObject(wrappedValue: container) 21 | } 22 | } 23 | 24 | // MARK: - View 25 | 26 | extension ___VARIABLE_sceneName___View: View { 27 | 28 | var body: some View { 29 | Text(state.text) 30 | .onAppear(perform: intent.viewOnAppear) 31 | .onDisappear(perform: intent.viewOnDisappear) 32 | } 33 | } 34 | 35 | #if DEBUG 36 | // MARK: - #Preview 37 | 38 | private struct PreviewView: View { 39 | 40 | var body: some View { 41 | ___VARIABLE_sceneName___View( 42 | data: ___VARIABLE_sceneName___ExternalData() 43 | ) 44 | } 45 | } 46 | 47 | #Preview { PreviewView() } 48 | #endif 49 | 50 | // MARK: - MVIContainer 51 | 52 | import SwiftUI 53 | import Combine 54 | 55 | final private class MVIContainer: ObservableObject { 56 | 57 | // MARK: Public 58 | 59 | let intent: Intent 60 | let model: Model 61 | 62 | // MARK: private 63 | 64 | private var cancellable: Set = [] 65 | 66 | // MARK: Life cycle 67 | 68 | init(intent: Intent, model: Model, modelChangePublisher: ObjectWillChangePublisher) { 69 | self.intent = intent 70 | self.model = model 71 | 72 | modelChangePublisher 73 | .receive(on: RunLoop.main) 74 | .sink(receiveValue: objectWillChange.send) 75 | .store(in: &cancellable) 76 | } 77 | } 78 | --------------------------------------------------------------------------------