├── 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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------