├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Demo
├── Demo.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── ViewModelable.xcscheme
├── Demo
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Car.swift
│ ├── CarViewController.swift
│ ├── CarViewModel.swift
│ └── Info.plist
├── DemoTests
│ ├── DemoTests.swift
│ └── Info.plist
├── ViewModelable
│ ├── Info.plist
│ └── ViewModelable.h
└── ViewModelableTests
│ ├── Info.plist
│ └── ViewModelableTests.swift
├── LICENSE
├── Package.swift
├── README.md
└── ViewModelable
├── ModelableCollectionViewController.swift
├── ModelableTableViewController.swift
├── ModelableViewController.swift
├── ViewModel.swift
└── ViewModelObservable.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | ## Build generated
5 | build/
6 | DerivedData
7 |
8 | ## Various settings
9 | *.pbxuser
10 | !default.pbxuser
11 | *.mode1v3
12 | !default.mode1v3
13 | *.mode2v3
14 | !default.mode2v3
15 | *.perspectivev3
16 | !default.perspectivev3
17 | xcuserdata
18 |
19 | ## Other
20 | *.xccheckout
21 | *.moved-aside
22 | *.xcuserstate
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | .build/
40 |
41 | # CocoaPods
42 | #
43 | # We recommend against adding the Pods directory to your .gitignore. However
44 | # you should judge for yourself, the pros and cons are mentioned at:
45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
46 | #
47 | Pods/
48 |
49 | # Carthage
50 | #
51 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
52 | # Carthage/Checkouts
53 |
54 | Carthage/Build
55 |
56 | # fastlane
57 | #
58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
59 | # screenshots whenever they are needed.
60 | # For more information about the recommended setup visit:
61 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
62 |
63 | fastlane/report.xml
64 | fastlane/Preview.html
65 | fastlane/screenshots
66 | fastlane/test_output
67 |
68 | #
69 | # Code Coverage
70 | #
71 | *.gcda
72 | *.gcno
73 |
74 | #
75 | # AppCode
76 | #
77 | .idea/
78 |
79 | #
80 | # Dominus
81 | #
82 | dominus.cfg
83 |
84 | #
85 | # KZBootstrap
86 | #
87 | KZBootstrapUserMacros.h
88 |
89 | #
90 | # R.swift
91 | #
92 | *.generated.swift
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 381BADEE1DE5B36200E52B80 /* ViewModelable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 381BADE51DE5B36200E52B80 /* ViewModelable.framework */; };
11 | 381BADF51DE5B36200E52B80 /* ViewModelableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381BADF41DE5B36200E52B80 /* ViewModelableTests.swift */; };
12 | 381BADF71DE5B36200E52B80 /* ViewModelable.h in Headers */ = {isa = PBXBuildFile; fileRef = 381BADE71DE5B36200E52B80 /* ViewModelable.h */; settings = {ATTRIBUTES = (Public, ); }; };
13 | 381BADFA1DE5B36200E52B80 /* ViewModelable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 381BADE51DE5B36200E52B80 /* ViewModelable.framework */; };
14 | 381BADFB1DE5B36200E52B80 /* ViewModelable.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 381BADE51DE5B36200E52B80 /* ViewModelable.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
15 | 381BAE031DE5B36900E52B80 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878E6BE1D058FF40068097F /* ViewModel.swift */; };
16 | 381BAE041DE5B36B00E52B80 /* ViewModelObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878E6BF1D058FF40068097F /* ViewModelObservable.swift */; };
17 | 381BAE051DE5B36F00E52B80 /* ModelableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878E6C21D05B4EB0068097F /* ModelableViewController.swift */; };
18 | 381F37111D31028E0049F1DE /* CarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381F37101D31028E0049F1DE /* CarViewModel.swift */; };
19 | 381F37131D310DF40049F1DE /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381F37121D310DF40049F1DE /* Car.swift */; };
20 | 3888B6A01CF393070095EBD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3888B69F1CF393070095EBD9 /* AppDelegate.swift */; };
21 | 3888B6A21CF393070095EBD9 /* CarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3888B6A11CF393070095EBD9 /* CarViewController.swift */; };
22 | 3888B6A51CF393070095EBD9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3888B6A31CF393070095EBD9 /* Main.storyboard */; };
23 | 3888B6A71CF393070095EBD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3888B6A61CF393070095EBD9 /* Assets.xcassets */; };
24 | 3888B6AA1CF393070095EBD9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3888B6A81CF393070095EBD9 /* LaunchScreen.storyboard */; };
25 | 3888B6B51CF393070095EBD9 /* DemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3888B6B41CF393070095EBD9 /* DemoTests.swift */; };
26 | 38F25CF81F557B860007AEFF /* ModelableTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F25CF61F557B600007AEFF /* ModelableTableViewController.swift */; };
27 | 38F25CFA1F557BC20007AEFF /* ModelableCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F25CF91F557BC20007AEFF /* ModelableCollectionViewController.swift */; };
28 | /* End PBXBuildFile section */
29 |
30 | /* Begin PBXContainerItemProxy section */
31 | 381BADEF1DE5B36200E52B80 /* PBXContainerItemProxy */ = {
32 | isa = PBXContainerItemProxy;
33 | containerPortal = 3888B6941CF393070095EBD9 /* Project object */;
34 | proxyType = 1;
35 | remoteGlobalIDString = 381BADE41DE5B36200E52B80;
36 | remoteInfo = ViewModelable;
37 | };
38 | 381BADF11DE5B36200E52B80 /* PBXContainerItemProxy */ = {
39 | isa = PBXContainerItemProxy;
40 | containerPortal = 3888B6941CF393070095EBD9 /* Project object */;
41 | proxyType = 1;
42 | remoteGlobalIDString = 3888B69B1CF393070095EBD9;
43 | remoteInfo = Demo;
44 | };
45 | 381BADF81DE5B36200E52B80 /* PBXContainerItemProxy */ = {
46 | isa = PBXContainerItemProxy;
47 | containerPortal = 3888B6941CF393070095EBD9 /* Project object */;
48 | proxyType = 1;
49 | remoteGlobalIDString = 381BADE41DE5B36200E52B80;
50 | remoteInfo = ViewModelable;
51 | };
52 | 3888B6B11CF393070095EBD9 /* PBXContainerItemProxy */ = {
53 | isa = PBXContainerItemProxy;
54 | containerPortal = 3888B6941CF393070095EBD9 /* Project object */;
55 | proxyType = 1;
56 | remoteGlobalIDString = 3888B69B1CF393070095EBD9;
57 | remoteInfo = Demo;
58 | };
59 | /* End PBXContainerItemProxy section */
60 |
61 | /* Begin PBXCopyFilesBuildPhase section */
62 | 381BAE011DE5B36200E52B80 /* Embed Frameworks */ = {
63 | isa = PBXCopyFilesBuildPhase;
64 | buildActionMask = 2147483647;
65 | dstPath = "";
66 | dstSubfolderSpec = 10;
67 | files = (
68 | 381BADFB1DE5B36200E52B80 /* ViewModelable.framework in Embed Frameworks */,
69 | );
70 | name = "Embed Frameworks";
71 | runOnlyForDeploymentPostprocessing = 0;
72 | };
73 | /* End PBXCopyFilesBuildPhase section */
74 |
75 | /* Begin PBXFileReference section */
76 | 381BADE51DE5B36200E52B80 /* ViewModelable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ViewModelable.framework; sourceTree = BUILT_PRODUCTS_DIR; };
77 | 381BADE71DE5B36200E52B80 /* ViewModelable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewModelable.h; sourceTree = ""; };
78 | 381BADE81DE5B36200E52B80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
79 | 381BADED1DE5B36200E52B80 /* ViewModelableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ViewModelableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
80 | 381BADF41DE5B36200E52B80 /* ViewModelableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelableTests.swift; sourceTree = ""; };
81 | 381BADF61DE5B36200E52B80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
82 | 381F37101D31028E0049F1DE /* CarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarViewModel.swift; sourceTree = ""; };
83 | 381F37121D310DF40049F1DE /* Car.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Car.swift; sourceTree = ""; };
84 | 3878E6BE1D058FF40068097F /* ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; };
85 | 3878E6BF1D058FF40068097F /* ViewModelObservable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelObservable.swift; sourceTree = ""; };
86 | 3878E6C21D05B4EB0068097F /* ModelableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelableViewController.swift; sourceTree = ""; };
87 | 3888B69C1CF393070095EBD9 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
88 | 3888B69F1CF393070095EBD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
89 | 3888B6A11CF393070095EBD9 /* CarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarViewController.swift; sourceTree = ""; };
90 | 3888B6A41CF393070095EBD9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
91 | 3888B6A61CF393070095EBD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
92 | 3888B6A91CF393070095EBD9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
93 | 3888B6AB1CF393070095EBD9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
94 | 3888B6B01CF393070095EBD9 /* DemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
95 | 3888B6B41CF393070095EBD9 /* DemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoTests.swift; sourceTree = ""; };
96 | 3888B6B61CF393070095EBD9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
97 | 38F25CF61F557B600007AEFF /* ModelableTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelableTableViewController.swift; sourceTree = ""; };
98 | 38F25CF91F557BC20007AEFF /* ModelableCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelableCollectionViewController.swift; sourceTree = ""; };
99 | /* End PBXFileReference section */
100 |
101 | /* Begin PBXFrameworksBuildPhase section */
102 | 381BADE11DE5B36200E52B80 /* Frameworks */ = {
103 | isa = PBXFrameworksBuildPhase;
104 | buildActionMask = 2147483647;
105 | files = (
106 | );
107 | runOnlyForDeploymentPostprocessing = 0;
108 | };
109 | 381BADEA1DE5B36200E52B80 /* Frameworks */ = {
110 | isa = PBXFrameworksBuildPhase;
111 | buildActionMask = 2147483647;
112 | files = (
113 | 381BADEE1DE5B36200E52B80 /* ViewModelable.framework in Frameworks */,
114 | );
115 | runOnlyForDeploymentPostprocessing = 0;
116 | };
117 | 3888B6991CF393070095EBD9 /* Frameworks */ = {
118 | isa = PBXFrameworksBuildPhase;
119 | buildActionMask = 2147483647;
120 | files = (
121 | 381BADFA1DE5B36200E52B80 /* ViewModelable.framework in Frameworks */,
122 | );
123 | runOnlyForDeploymentPostprocessing = 0;
124 | };
125 | 3888B6AD1CF393070095EBD9 /* Frameworks */ = {
126 | isa = PBXFrameworksBuildPhase;
127 | buildActionMask = 2147483647;
128 | files = (
129 | );
130 | runOnlyForDeploymentPostprocessing = 0;
131 | };
132 | /* End PBXFrameworksBuildPhase section */
133 |
134 | /* Begin PBXGroup section */
135 | 381BADE61DE5B36200E52B80 /* ViewModelable */ = {
136 | isa = PBXGroup;
137 | children = (
138 | 381BADE71DE5B36200E52B80 /* ViewModelable.h */,
139 | 381BADE81DE5B36200E52B80 /* Info.plist */,
140 | );
141 | path = ViewModelable;
142 | sourceTree = "";
143 | };
144 | 381BADF31DE5B36200E52B80 /* ViewModelableTests */ = {
145 | isa = PBXGroup;
146 | children = (
147 | 381BADF41DE5B36200E52B80 /* ViewModelableTests.swift */,
148 | 381BADF61DE5B36200E52B80 /* Info.plist */,
149 | );
150 | path = ViewModelableTests;
151 | sourceTree = "";
152 | };
153 | 3878E6BD1D058FF40068097F /* ViewModelable */ = {
154 | isa = PBXGroup;
155 | children = (
156 | 3878E6BE1D058FF40068097F /* ViewModel.swift */,
157 | 3878E6BF1D058FF40068097F /* ViewModelObservable.swift */,
158 | 38F25CF91F557BC20007AEFF /* ModelableCollectionViewController.swift */,
159 | 38F25CF61F557B600007AEFF /* ModelableTableViewController.swift */,
160 | 3878E6C21D05B4EB0068097F /* ModelableViewController.swift */,
161 | );
162 | name = ViewModelable;
163 | path = ../ViewModelable;
164 | sourceTree = "";
165 | };
166 | 3888B6931CF393070095EBD9 = {
167 | isa = PBXGroup;
168 | children = (
169 | 3878E6BD1D058FF40068097F /* ViewModelable */,
170 | 3888B69E1CF393070095EBD9 /* Demo */,
171 | 3888B6B31CF393070095EBD9 /* DemoTests */,
172 | 381BADE61DE5B36200E52B80 /* ViewModelable */,
173 | 381BADF31DE5B36200E52B80 /* ViewModelableTests */,
174 | 3888B69D1CF393070095EBD9 /* Products */,
175 | );
176 | sourceTree = "";
177 | };
178 | 3888B69D1CF393070095EBD9 /* Products */ = {
179 | isa = PBXGroup;
180 | children = (
181 | 3888B69C1CF393070095EBD9 /* Demo.app */,
182 | 3888B6B01CF393070095EBD9 /* DemoTests.xctest */,
183 | 381BADE51DE5B36200E52B80 /* ViewModelable.framework */,
184 | 381BADED1DE5B36200E52B80 /* ViewModelableTests.xctest */,
185 | );
186 | name = Products;
187 | sourceTree = "";
188 | };
189 | 3888B69E1CF393070095EBD9 /* Demo */ = {
190 | isa = PBXGroup;
191 | children = (
192 | 3888B69F1CF393070095EBD9 /* AppDelegate.swift */,
193 | 3888B6AB1CF393070095EBD9 /* Info.plist */,
194 | 381F37121D310DF40049F1DE /* Car.swift */,
195 | 3888B6A11CF393070095EBD9 /* CarViewController.swift */,
196 | 381F37101D31028E0049F1DE /* CarViewModel.swift */,
197 | 3888B6A31CF393070095EBD9 /* Main.storyboard */,
198 | 3888B6A61CF393070095EBD9 /* Assets.xcassets */,
199 | 3888B6A81CF393070095EBD9 /* LaunchScreen.storyboard */,
200 | );
201 | path = Demo;
202 | sourceTree = "";
203 | };
204 | 3888B6B31CF393070095EBD9 /* DemoTests */ = {
205 | isa = PBXGroup;
206 | children = (
207 | 3888B6B41CF393070095EBD9 /* DemoTests.swift */,
208 | 3888B6B61CF393070095EBD9 /* Info.plist */,
209 | );
210 | path = DemoTests;
211 | sourceTree = SOURCE_ROOT;
212 | };
213 | /* End PBXGroup section */
214 |
215 | /* Begin PBXHeadersBuildPhase section */
216 | 381BADE21DE5B36200E52B80 /* Headers */ = {
217 | isa = PBXHeadersBuildPhase;
218 | buildActionMask = 2147483647;
219 | files = (
220 | 381BADF71DE5B36200E52B80 /* ViewModelable.h in Headers */,
221 | );
222 | runOnlyForDeploymentPostprocessing = 0;
223 | };
224 | /* End PBXHeadersBuildPhase section */
225 |
226 | /* Begin PBXNativeTarget section */
227 | 381BADE41DE5B36200E52B80 /* ViewModelable */ = {
228 | isa = PBXNativeTarget;
229 | buildConfigurationList = 381BAE001DE5B36200E52B80 /* Build configuration list for PBXNativeTarget "ViewModelable" */;
230 | buildPhases = (
231 | 381BADE01DE5B36200E52B80 /* Sources */,
232 | 381BADE11DE5B36200E52B80 /* Frameworks */,
233 | 381BADE21DE5B36200E52B80 /* Headers */,
234 | 381BADE31DE5B36200E52B80 /* Resources */,
235 | );
236 | buildRules = (
237 | );
238 | dependencies = (
239 | );
240 | name = ViewModelable;
241 | productName = ViewModelable;
242 | productReference = 381BADE51DE5B36200E52B80 /* ViewModelable.framework */;
243 | productType = "com.apple.product-type.framework";
244 | };
245 | 381BADEC1DE5B36200E52B80 /* ViewModelableTests */ = {
246 | isa = PBXNativeTarget;
247 | buildConfigurationList = 381BAE021DE5B36200E52B80 /* Build configuration list for PBXNativeTarget "ViewModelableTests" */;
248 | buildPhases = (
249 | 381BADE91DE5B36200E52B80 /* Sources */,
250 | 381BADEA1DE5B36200E52B80 /* Frameworks */,
251 | 381BADEB1DE5B36200E52B80 /* Resources */,
252 | );
253 | buildRules = (
254 | );
255 | dependencies = (
256 | 381BADF01DE5B36200E52B80 /* PBXTargetDependency */,
257 | 381BADF21DE5B36200E52B80 /* PBXTargetDependency */,
258 | );
259 | name = ViewModelableTests;
260 | productName = ViewModelableTests;
261 | productReference = 381BADED1DE5B36200E52B80 /* ViewModelableTests.xctest */;
262 | productType = "com.apple.product-type.bundle.unit-test";
263 | };
264 | 3888B69B1CF393070095EBD9 /* Demo */ = {
265 | isa = PBXNativeTarget;
266 | buildConfigurationList = 3888B6B91CF393070095EBD9 /* Build configuration list for PBXNativeTarget "Demo" */;
267 | buildPhases = (
268 | 3888B6981CF393070095EBD9 /* Sources */,
269 | 3888B6991CF393070095EBD9 /* Frameworks */,
270 | 3888B69A1CF393070095EBD9 /* Resources */,
271 | 381BAE011DE5B36200E52B80 /* Embed Frameworks */,
272 | );
273 | buildRules = (
274 | );
275 | dependencies = (
276 | 381BADF91DE5B36200E52B80 /* PBXTargetDependency */,
277 | );
278 | name = Demo;
279 | productName = Demo;
280 | productReference = 3888B69C1CF393070095EBD9 /* Demo.app */;
281 | productType = "com.apple.product-type.application";
282 | };
283 | 3888B6AF1CF393070095EBD9 /* DemoTests */ = {
284 | isa = PBXNativeTarget;
285 | buildConfigurationList = 3888B6BC1CF393070095EBD9 /* Build configuration list for PBXNativeTarget "DemoTests" */;
286 | buildPhases = (
287 | 3888B6AC1CF393070095EBD9 /* Sources */,
288 | 3888B6AD1CF393070095EBD9 /* Frameworks */,
289 | 3888B6AE1CF393070095EBD9 /* Resources */,
290 | );
291 | buildRules = (
292 | );
293 | dependencies = (
294 | 3888B6B21CF393070095EBD9 /* PBXTargetDependency */,
295 | );
296 | name = DemoTests;
297 | productName = DemoTests;
298 | productReference = 3888B6B01CF393070095EBD9 /* DemoTests.xctest */;
299 | productType = "com.apple.product-type.bundle.unit-test";
300 | };
301 | /* End PBXNativeTarget section */
302 |
303 | /* Begin PBXProject section */
304 | 3888B6941CF393070095EBD9 /* Project object */ = {
305 | isa = PBXProject;
306 | attributes = {
307 | BuildIndependentTargetsInParallel = YES;
308 | LastSwiftUpdateCheck = 0810;
309 | LastUpgradeCheck = 1620;
310 | ORGANIZATIONNAME = "Unified Sense";
311 | TargetAttributes = {
312 | 381BADE41DE5B36200E52B80 = {
313 | CreatedOnToolsVersion = 8.1;
314 | DevelopmentTeam = 289M6XEDV4;
315 | LastSwiftMigration = 1020;
316 | ProvisioningStyle = Automatic;
317 | };
318 | 381BADEC1DE5B36200E52B80 = {
319 | CreatedOnToolsVersion = 8.1;
320 | DevelopmentTeam = 289M6XEDV4;
321 | LastSwiftMigration = 1020;
322 | ProvisioningStyle = Automatic;
323 | TestTargetID = 3888B69B1CF393070095EBD9;
324 | };
325 | 3888B69B1CF393070095EBD9 = {
326 | CreatedOnToolsVersion = 7.3.1;
327 | DevelopmentTeam = 289M6XEDV4;
328 | LastSwiftMigration = 1020;
329 | };
330 | 3888B6AF1CF393070095EBD9 = {
331 | CreatedOnToolsVersion = 7.3.1;
332 | DevelopmentTeam = 289M6XEDV4;
333 | LastSwiftMigration = 1020;
334 | TestTargetID = 3888B69B1CF393070095EBD9;
335 | };
336 | };
337 | };
338 | buildConfigurationList = 3888B6971CF393070095EBD9 /* Build configuration list for PBXProject "Demo" */;
339 | compatibilityVersion = "Xcode 3.2";
340 | developmentRegion = en;
341 | hasScannedForEncodings = 0;
342 | knownRegions = (
343 | en,
344 | Base,
345 | );
346 | mainGroup = 3888B6931CF393070095EBD9;
347 | productRefGroup = 3888B69D1CF393070095EBD9 /* Products */;
348 | projectDirPath = "";
349 | projectRoot = "";
350 | targets = (
351 | 3888B69B1CF393070095EBD9 /* Demo */,
352 | 3888B6AF1CF393070095EBD9 /* DemoTests */,
353 | 381BADE41DE5B36200E52B80 /* ViewModelable */,
354 | 381BADEC1DE5B36200E52B80 /* ViewModelableTests */,
355 | );
356 | };
357 | /* End PBXProject section */
358 |
359 | /* Begin PBXResourcesBuildPhase section */
360 | 381BADE31DE5B36200E52B80 /* Resources */ = {
361 | isa = PBXResourcesBuildPhase;
362 | buildActionMask = 2147483647;
363 | files = (
364 | );
365 | runOnlyForDeploymentPostprocessing = 0;
366 | };
367 | 381BADEB1DE5B36200E52B80 /* Resources */ = {
368 | isa = PBXResourcesBuildPhase;
369 | buildActionMask = 2147483647;
370 | files = (
371 | );
372 | runOnlyForDeploymentPostprocessing = 0;
373 | };
374 | 3888B69A1CF393070095EBD9 /* Resources */ = {
375 | isa = PBXResourcesBuildPhase;
376 | buildActionMask = 2147483647;
377 | files = (
378 | 3888B6AA1CF393070095EBD9 /* LaunchScreen.storyboard in Resources */,
379 | 3888B6A71CF393070095EBD9 /* Assets.xcassets in Resources */,
380 | 3888B6A51CF393070095EBD9 /* Main.storyboard in Resources */,
381 | );
382 | runOnlyForDeploymentPostprocessing = 0;
383 | };
384 | 3888B6AE1CF393070095EBD9 /* Resources */ = {
385 | isa = PBXResourcesBuildPhase;
386 | buildActionMask = 2147483647;
387 | files = (
388 | );
389 | runOnlyForDeploymentPostprocessing = 0;
390 | };
391 | /* End PBXResourcesBuildPhase section */
392 |
393 | /* Begin PBXSourcesBuildPhase section */
394 | 381BADE01DE5B36200E52B80 /* Sources */ = {
395 | isa = PBXSourcesBuildPhase;
396 | buildActionMask = 2147483647;
397 | files = (
398 | 381BAE031DE5B36900E52B80 /* ViewModel.swift in Sources */,
399 | 38F25CF81F557B860007AEFF /* ModelableTableViewController.swift in Sources */,
400 | 38F25CFA1F557BC20007AEFF /* ModelableCollectionViewController.swift in Sources */,
401 | 381BAE041DE5B36B00E52B80 /* ViewModelObservable.swift in Sources */,
402 | 381BAE051DE5B36F00E52B80 /* ModelableViewController.swift in Sources */,
403 | );
404 | runOnlyForDeploymentPostprocessing = 0;
405 | };
406 | 381BADE91DE5B36200E52B80 /* Sources */ = {
407 | isa = PBXSourcesBuildPhase;
408 | buildActionMask = 2147483647;
409 | files = (
410 | 381BADF51DE5B36200E52B80 /* ViewModelableTests.swift in Sources */,
411 | );
412 | runOnlyForDeploymentPostprocessing = 0;
413 | };
414 | 3888B6981CF393070095EBD9 /* Sources */ = {
415 | isa = PBXSourcesBuildPhase;
416 | buildActionMask = 2147483647;
417 | files = (
418 | 3888B6A21CF393070095EBD9 /* CarViewController.swift in Sources */,
419 | 381F37111D31028E0049F1DE /* CarViewModel.swift in Sources */,
420 | 381F37131D310DF40049F1DE /* Car.swift in Sources */,
421 | 3888B6A01CF393070095EBD9 /* AppDelegate.swift in Sources */,
422 | );
423 | runOnlyForDeploymentPostprocessing = 0;
424 | };
425 | 3888B6AC1CF393070095EBD9 /* Sources */ = {
426 | isa = PBXSourcesBuildPhase;
427 | buildActionMask = 2147483647;
428 | files = (
429 | 3888B6B51CF393070095EBD9 /* DemoTests.swift in Sources */,
430 | );
431 | runOnlyForDeploymentPostprocessing = 0;
432 | };
433 | /* End PBXSourcesBuildPhase section */
434 |
435 | /* Begin PBXTargetDependency section */
436 | 381BADF01DE5B36200E52B80 /* PBXTargetDependency */ = {
437 | isa = PBXTargetDependency;
438 | target = 381BADE41DE5B36200E52B80 /* ViewModelable */;
439 | targetProxy = 381BADEF1DE5B36200E52B80 /* PBXContainerItemProxy */;
440 | };
441 | 381BADF21DE5B36200E52B80 /* PBXTargetDependency */ = {
442 | isa = PBXTargetDependency;
443 | target = 3888B69B1CF393070095EBD9 /* Demo */;
444 | targetProxy = 381BADF11DE5B36200E52B80 /* PBXContainerItemProxy */;
445 | };
446 | 381BADF91DE5B36200E52B80 /* PBXTargetDependency */ = {
447 | isa = PBXTargetDependency;
448 | target = 381BADE41DE5B36200E52B80 /* ViewModelable */;
449 | targetProxy = 381BADF81DE5B36200E52B80 /* PBXContainerItemProxy */;
450 | };
451 | 3888B6B21CF393070095EBD9 /* PBXTargetDependency */ = {
452 | isa = PBXTargetDependency;
453 | target = 3888B69B1CF393070095EBD9 /* Demo */;
454 | targetProxy = 3888B6B11CF393070095EBD9 /* PBXContainerItemProxy */;
455 | };
456 | /* End PBXTargetDependency section */
457 |
458 | /* Begin PBXVariantGroup section */
459 | 3888B6A31CF393070095EBD9 /* Main.storyboard */ = {
460 | isa = PBXVariantGroup;
461 | children = (
462 | 3888B6A41CF393070095EBD9 /* Base */,
463 | );
464 | name = Main.storyboard;
465 | sourceTree = "";
466 | };
467 | 3888B6A81CF393070095EBD9 /* LaunchScreen.storyboard */ = {
468 | isa = PBXVariantGroup;
469 | children = (
470 | 3888B6A91CF393070095EBD9 /* Base */,
471 | );
472 | name = LaunchScreen.storyboard;
473 | sourceTree = "";
474 | };
475 | /* End PBXVariantGroup section */
476 |
477 | /* Begin XCBuildConfiguration section */
478 | 381BADFC1DE5B36200E52B80 /* Debug */ = {
479 | isa = XCBuildConfiguration;
480 | buildSettings = {
481 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
482 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
483 | CODE_SIGN_IDENTITY = "";
484 | CURRENT_PROJECT_VERSION = 1;
485 | DEFINES_MODULE = YES;
486 | DEVELOPMENT_TEAM = 289M6XEDV4;
487 | DYLIB_COMPATIBILITY_VERSION = 1;
488 | DYLIB_CURRENT_VERSION = 1;
489 | DYLIB_INSTALL_NAME_BASE = "@rpath";
490 | ENABLE_MODULE_VERIFIER = YES;
491 | INFOPLIST_FILE = ViewModelable/Info.plist;
492 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
493 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
494 | LD_RUNPATH_SEARCH_PATHS = (
495 | "$(inherited)",
496 | "@executable_path/Frameworks",
497 | "@loader_path/Frameworks",
498 | );
499 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
500 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable;
501 | PRODUCT_NAME = "$(TARGET_NAME)";
502 | SKIP_INSTALL = YES;
503 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
504 | SWIFT_VERSION = 5.0;
505 | VERSIONING_SYSTEM = "apple-generic";
506 | VERSION_INFO_PREFIX = "";
507 | };
508 | name = Debug;
509 | };
510 | 381BADFD1DE5B36200E52B80 /* Release */ = {
511 | isa = XCBuildConfiguration;
512 | buildSettings = {
513 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
514 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
515 | CODE_SIGN_IDENTITY = "";
516 | CURRENT_PROJECT_VERSION = 1;
517 | DEFINES_MODULE = YES;
518 | DEVELOPMENT_TEAM = 289M6XEDV4;
519 | DYLIB_COMPATIBILITY_VERSION = 1;
520 | DYLIB_CURRENT_VERSION = 1;
521 | DYLIB_INSTALL_NAME_BASE = "@rpath";
522 | ENABLE_MODULE_VERIFIER = YES;
523 | INFOPLIST_FILE = ViewModelable/Info.plist;
524 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
525 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
526 | LD_RUNPATH_SEARCH_PATHS = (
527 | "$(inherited)",
528 | "@executable_path/Frameworks",
529 | "@loader_path/Frameworks",
530 | );
531 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
532 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable;
533 | PRODUCT_NAME = "$(TARGET_NAME)";
534 | SKIP_INSTALL = YES;
535 | SWIFT_VERSION = 5.0;
536 | VERSIONING_SYSTEM = "apple-generic";
537 | VERSION_INFO_PREFIX = "";
538 | };
539 | name = Release;
540 | };
541 | 381BADFE1DE5B36200E52B80 /* Debug */ = {
542 | isa = XCBuildConfiguration;
543 | buildSettings = {
544 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
545 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
546 | DEVELOPMENT_TEAM = 289M6XEDV4;
547 | INFOPLIST_FILE = ViewModelableTests/Info.plist;
548 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
549 | LD_RUNPATH_SEARCH_PATHS = (
550 | "$(inherited)",
551 | "@executable_path/Frameworks",
552 | "@loader_path/Frameworks",
553 | );
554 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelableTests;
555 | PRODUCT_NAME = "$(TARGET_NAME)";
556 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
557 | SWIFT_VERSION = 5.0;
558 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo";
559 | };
560 | name = Debug;
561 | };
562 | 381BADFF1DE5B36200E52B80 /* Release */ = {
563 | isa = XCBuildConfiguration;
564 | buildSettings = {
565 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
566 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
567 | DEVELOPMENT_TEAM = 289M6XEDV4;
568 | INFOPLIST_FILE = ViewModelableTests/Info.plist;
569 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
570 | LD_RUNPATH_SEARCH_PATHS = (
571 | "$(inherited)",
572 | "@executable_path/Frameworks",
573 | "@loader_path/Frameworks",
574 | );
575 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelableTests;
576 | PRODUCT_NAME = "$(TARGET_NAME)";
577 | SWIFT_VERSION = 5.0;
578 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo";
579 | };
580 | name = Release;
581 | };
582 | 3888B6B71CF393070095EBD9 /* Debug */ = {
583 | isa = XCBuildConfiguration;
584 | buildSettings = {
585 | ALWAYS_SEARCH_USER_PATHS = NO;
586 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
587 | CLANG_ANALYZER_NONNULL = YES;
588 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
589 | CLANG_CXX_LIBRARY = "libc++";
590 | CLANG_ENABLE_MODULES = YES;
591 | CLANG_ENABLE_OBJC_ARC = YES;
592 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
593 | CLANG_WARN_BOOL_CONVERSION = YES;
594 | CLANG_WARN_COMMA = YES;
595 | CLANG_WARN_CONSTANT_CONVERSION = YES;
596 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
597 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
598 | CLANG_WARN_EMPTY_BODY = YES;
599 | CLANG_WARN_ENUM_CONVERSION = YES;
600 | CLANG_WARN_INFINITE_RECURSION = YES;
601 | CLANG_WARN_INT_CONVERSION = YES;
602 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
603 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
604 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
605 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
606 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
607 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
608 | CLANG_WARN_STRICT_PROTOTYPES = YES;
609 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
610 | CLANG_WARN_UNREACHABLE_CODE = YES;
611 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
612 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
613 | COPY_PHASE_STRIP = NO;
614 | DEBUG_INFORMATION_FORMAT = dwarf;
615 | ENABLE_STRICT_OBJC_MSGSEND = YES;
616 | ENABLE_TESTABILITY = YES;
617 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
618 | GCC_C_LANGUAGE_STANDARD = gnu99;
619 | GCC_DYNAMIC_NO_PIC = NO;
620 | GCC_NO_COMMON_BLOCKS = YES;
621 | GCC_OPTIMIZATION_LEVEL = 0;
622 | GCC_PREPROCESSOR_DEFINITIONS = (
623 | "DEBUG=1",
624 | "$(inherited)",
625 | );
626 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
627 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
628 | GCC_WARN_UNDECLARED_SELECTOR = YES;
629 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
630 | GCC_WARN_UNUSED_FUNCTION = YES;
631 | GCC_WARN_UNUSED_VARIABLE = YES;
632 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
633 | MTL_ENABLE_DEBUG_INFO = YES;
634 | ONLY_ACTIVE_ARCH = YES;
635 | SDKROOT = iphoneos;
636 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
637 | SWIFT_VERSION = 4.2;
638 | TARGETED_DEVICE_FAMILY = "1,2";
639 | };
640 | name = Debug;
641 | };
642 | 3888B6B81CF393070095EBD9 /* Release */ = {
643 | isa = XCBuildConfiguration;
644 | buildSettings = {
645 | ALWAYS_SEARCH_USER_PATHS = NO;
646 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
647 | CLANG_ANALYZER_NONNULL = YES;
648 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
649 | CLANG_CXX_LIBRARY = "libc++";
650 | CLANG_ENABLE_MODULES = YES;
651 | CLANG_ENABLE_OBJC_ARC = YES;
652 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
653 | CLANG_WARN_BOOL_CONVERSION = YES;
654 | CLANG_WARN_COMMA = YES;
655 | CLANG_WARN_CONSTANT_CONVERSION = YES;
656 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
657 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
658 | CLANG_WARN_EMPTY_BODY = YES;
659 | CLANG_WARN_ENUM_CONVERSION = YES;
660 | CLANG_WARN_INFINITE_RECURSION = YES;
661 | CLANG_WARN_INT_CONVERSION = YES;
662 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
663 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
664 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
665 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
666 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
667 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
668 | CLANG_WARN_STRICT_PROTOTYPES = YES;
669 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
670 | CLANG_WARN_UNREACHABLE_CODE = YES;
671 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
672 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
673 | COPY_PHASE_STRIP = NO;
674 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
675 | ENABLE_NS_ASSERTIONS = NO;
676 | ENABLE_STRICT_OBJC_MSGSEND = YES;
677 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
678 | GCC_C_LANGUAGE_STANDARD = gnu99;
679 | GCC_NO_COMMON_BLOCKS = YES;
680 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
681 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
682 | GCC_WARN_UNDECLARED_SELECTOR = YES;
683 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
684 | GCC_WARN_UNUSED_FUNCTION = YES;
685 | GCC_WARN_UNUSED_VARIABLE = YES;
686 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
687 | MTL_ENABLE_DEBUG_INFO = NO;
688 | SDKROOT = iphoneos;
689 | SWIFT_COMPILATION_MODE = wholemodule;
690 | SWIFT_OPTIMIZATION_LEVEL = "-O";
691 | SWIFT_VERSION = 4.2;
692 | TARGETED_DEVICE_FAMILY = "1,2";
693 | VALIDATE_PRODUCT = YES;
694 | };
695 | name = Release;
696 | };
697 | 3888B6BA1CF393070095EBD9 /* Debug */ = {
698 | isa = XCBuildConfiguration;
699 | buildSettings = {
700 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
701 | DEVELOPMENT_TEAM = 289M6XEDV4;
702 | INFOPLIST_FILE = Demo/Info.plist;
703 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
704 | LD_RUNPATH_SEARCH_PATHS = (
705 | "$(inherited)",
706 | "@executable_path/Frameworks",
707 | );
708 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable.Demo;
709 | PRODUCT_NAME = "$(TARGET_NAME)";
710 | SWIFT_VERSION = 5.0;
711 | };
712 | name = Debug;
713 | };
714 | 3888B6BB1CF393070095EBD9 /* Release */ = {
715 | isa = XCBuildConfiguration;
716 | buildSettings = {
717 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
718 | DEVELOPMENT_TEAM = 289M6XEDV4;
719 | INFOPLIST_FILE = Demo/Info.plist;
720 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
721 | LD_RUNPATH_SEARCH_PATHS = (
722 | "$(inherited)",
723 | "@executable_path/Frameworks",
724 | );
725 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable.Demo;
726 | PRODUCT_NAME = "$(TARGET_NAME)";
727 | SWIFT_VERSION = 5.0;
728 | };
729 | name = Release;
730 | };
731 | 3888B6BD1CF393070095EBD9 /* Debug */ = {
732 | isa = XCBuildConfiguration;
733 | buildSettings = {
734 | BUNDLE_LOADER = "$(TEST_HOST)";
735 | DEVELOPMENT_TEAM = 289M6XEDV4;
736 | INFOPLIST_FILE = DemoTests/Info.plist;
737 | LD_RUNPATH_SEARCH_PATHS = (
738 | "$(inherited)",
739 | "@executable_path/Frameworks",
740 | "@loader_path/Frameworks",
741 | );
742 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable.DemoTests;
743 | PRODUCT_NAME = "$(TARGET_NAME)";
744 | SWIFT_VERSION = 5.0;
745 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo";
746 | };
747 | name = Debug;
748 | };
749 | 3888B6BE1CF393070095EBD9 /* Release */ = {
750 | isa = XCBuildConfiguration;
751 | buildSettings = {
752 | BUNDLE_LOADER = "$(TEST_HOST)";
753 | DEVELOPMENT_TEAM = 289M6XEDV4;
754 | INFOPLIST_FILE = DemoTests/Info.plist;
755 | LD_RUNPATH_SEARCH_PATHS = (
756 | "$(inherited)",
757 | "@executable_path/Frameworks",
758 | "@loader_path/Frameworks",
759 | );
760 | PRODUCT_BUNDLE_IDENTIFIER = com.unifiedsense.ViewModelable.DemoTests;
761 | PRODUCT_NAME = "$(TARGET_NAME)";
762 | SWIFT_VERSION = 5.0;
763 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo";
764 | };
765 | name = Release;
766 | };
767 | /* End XCBuildConfiguration section */
768 |
769 | /* Begin XCConfigurationList section */
770 | 381BAE001DE5B36200E52B80 /* Build configuration list for PBXNativeTarget "ViewModelable" */ = {
771 | isa = XCConfigurationList;
772 | buildConfigurations = (
773 | 381BADFC1DE5B36200E52B80 /* Debug */,
774 | 381BADFD1DE5B36200E52B80 /* Release */,
775 | );
776 | defaultConfigurationIsVisible = 0;
777 | defaultConfigurationName = Release;
778 | };
779 | 381BAE021DE5B36200E52B80 /* Build configuration list for PBXNativeTarget "ViewModelableTests" */ = {
780 | isa = XCConfigurationList;
781 | buildConfigurations = (
782 | 381BADFE1DE5B36200E52B80 /* Debug */,
783 | 381BADFF1DE5B36200E52B80 /* Release */,
784 | );
785 | defaultConfigurationIsVisible = 0;
786 | defaultConfigurationName = Release;
787 | };
788 | 3888B6971CF393070095EBD9 /* Build configuration list for PBXProject "Demo" */ = {
789 | isa = XCConfigurationList;
790 | buildConfigurations = (
791 | 3888B6B71CF393070095EBD9 /* Debug */,
792 | 3888B6B81CF393070095EBD9 /* Release */,
793 | );
794 | defaultConfigurationIsVisible = 0;
795 | defaultConfigurationName = Release;
796 | };
797 | 3888B6B91CF393070095EBD9 /* Build configuration list for PBXNativeTarget "Demo" */ = {
798 | isa = XCConfigurationList;
799 | buildConfigurations = (
800 | 3888B6BA1CF393070095EBD9 /* Debug */,
801 | 3888B6BB1CF393070095EBD9 /* Release */,
802 | );
803 | defaultConfigurationIsVisible = 0;
804 | defaultConfigurationName = Release;
805 | };
806 | 3888B6BC1CF393070095EBD9 /* Build configuration list for PBXNativeTarget "DemoTests" */ = {
807 | isa = XCConfigurationList;
808 | buildConfigurations = (
809 | 3888B6BD1CF393070095EBD9 /* Debug */,
810 | 3888B6BE1CF393070095EBD9 /* Release */,
811 | );
812 | defaultConfigurationIsVisible = 0;
813 | defaultConfigurationName = Release;
814 | };
815 | /* End XCConfigurationList section */
816 | };
817 | rootObject = 3888B6941CF393070095EBD9 /* Project object */;
818 | }
819 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcshareddata/xcschemes/ViewModelable.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/Demo/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by Dal Rupnik on 23/05/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Demo/Demo/Car.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Car.swift
3 | // Demo
4 | //
5 | // Created by Dal Rupnik on 09/07/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | //
10 | // MARK: Definition
11 | //
12 |
13 | // A simple car model that describes car specifications. Does not represent a real model, rather an approximation,
14 | // for view model example purpose.
15 | //
16 | // Data specification taken from https://www.quora.com/What-do-the-specifications-of-a-car-actually-mean
17 | //
18 |
19 | enum BodyType : String {
20 | case mini, small, medium, large, executive, luxury, coupe, roadster, stationWagon, minivan, SUV, pickup
21 | }
22 |
23 | //
24 | // Car Model
25 | //
26 | struct Car {
27 | var make : String
28 | var model : String
29 | var body : BodyType
30 | var doorCount : UInt
31 |
32 | var engine : Engine
33 | var drivetrain: Drivetrain
34 | var transmission : Transmission
35 | }
36 |
37 | enum Drivetrain : String {
38 | case fwd, rwd, awd
39 | }
40 |
41 | enum Transmission : String {
42 | case manual, automatic
43 | }
44 |
45 | //
46 | // Engine specifications
47 | //
48 |
49 | enum EngineType : String {
50 | case inLine, vType, horizontal
51 | }
52 |
53 | struct Engine {
54 | var type : EngineType
55 | var displacement : UInt // In cc
56 | var brakeHorsepower : UInt // In BHP
57 | var torque : Double // In N/m
58 | var turbocharged : Bool
59 | }
60 |
61 | //
62 | // MARK: Factory methods
63 | //
64 |
65 | extension Car {
66 | static func lamborghiniAvendator() -> Car {
67 | let engine = Engine(type: .vType, displacement: 6498, brakeHorsepower: 690, torque: 689, turbocharged: false)
68 |
69 | return Car(make: "Lamborghini", model: "Aventador", body: .coupe, doorCount: 2, engine: engine, drivetrain: .awd, transmission: .automatic)
70 | }
71 |
72 | static func fordFocusRs() -> Car {
73 | let engine = Engine(type: .inLine, displacement: 2298, brakeHorsepower: 345, torque: 475, turbocharged: true)
74 |
75 | return Car(make: "Ford", model: "Focus RS", body: .small, doorCount: 4, engine: engine, drivetrain: .awd, transmission: .manual)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Demo/Demo/CarViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CarViewController.swift
3 | // Demo
4 | //
5 | // Created by Dal Rupnik on 23/05/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ViewModelable
11 |
12 | class CarViewController: ModelableViewController {
13 |
14 | //
15 | // MARK: - Outlets
16 | //
17 |
18 | @IBOutlet private weak var activityIndicatorView: UIActivityIndicatorView!
19 | @IBOutlet private weak var topLabel: UILabel!
20 | @IBOutlet private weak var bottomLabel: UILabel!
21 |
22 | //
23 | // MARK: - ViewModelObservable
24 | //
25 |
26 | override func viewModelDidSetup(viewModel: CarViewModel) {
27 | super.viewModelDidSetup(viewModel: viewModel)
28 |
29 | //
30 | // Called once after viewDidLoad, view model will be in .setuped state.
31 | //
32 |
33 | print("[+] ViewModel: \(viewModel) did setup.")
34 |
35 | update()
36 | }
37 |
38 | override func viewModelDidLoad(viewModel: CarViewModel) {
39 | super.viewModelDidLoad(viewModel: viewModel)
40 | //
41 | // Can be called anytime after viewWillAppear (asychronously) and can be called multiple times.
42 | //
43 |
44 | print("[+] ViewModel: \(viewModel) did load.")
45 |
46 | update()
47 | }
48 |
49 | override func viewModelDidUpdate(viewModel: CarViewModel, updates: [String : Any]) {
50 | super.viewModelDidUpdate(viewModel: viewModel, updates: updates)
51 |
52 | //
53 | // Can be called anytime after viewModelDidLoad is called and can be called multiple times.
54 | //
55 | }
56 |
57 | override func viewModelDidUnload(viewModel: CarViewModel) {
58 | super.viewModelDidUnload(viewModel: viewModel)
59 | //
60 | // Will be called after viewWillDisappear, view model transitioned to .setuped state.
61 | //
62 | }
63 |
64 | //
65 | // MARK: Private Methods
66 | //
67 |
68 | func update() {
69 | topLabel.text = viewModel.carDescription
70 | bottomLabel.text = viewModel.engineDescription
71 |
72 | activityIndicatorView.isHidden = !viewModel.loading
73 | }
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/Demo/Demo/CarViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CarViewModel.swift
3 | // Demo
4 | //
5 | // Created by Dal Rupnik on 09/07/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ViewModelable
11 |
12 | class CarViewModel : ViewModel {
13 | //
14 | // MARK: Input of the view model, must be
15 | //
16 |
17 | var car : Car?
18 |
19 | //
20 | // MARK: Output of view model, must be populated at all times, so screen can be
21 | //
22 |
23 | private(set) var loading : Bool = false
24 |
25 | private(set) var carDescription = ""
26 | private(set) var engineDescription = ""
27 |
28 | override func startSetup() {
29 | super.startSetup()
30 |
31 | //
32 | // View model should handle data, here we just pull a car if it was not set.
33 | //
34 | loading = true
35 | }
36 |
37 | override func startLoading() {
38 | loading = true
39 | carDescription = "Loading..."
40 | engineDescription = ""
41 |
42 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
43 | self.loading = false
44 |
45 | if self.car == nil {
46 | self.car = Car.lamborghiniAvendator()
47 | }
48 |
49 | //
50 | // Call to finish loading, to ensure state transition is correct.
51 | //
52 | self.finishLoading()
53 | }
54 |
55 | }
56 |
57 | override func updateOutput() {
58 |
59 | //
60 | // This method is called multiple times during state transitions and
61 | // should just set output variables in a synchronous way.
62 | //
63 |
64 | if let car = car {
65 | carDescription = "\(car.make) \(car.model)"
66 | engineDescription = "\(car.engine.displacement) cc, \(car.engine.brakeHorsepower) BHP"
67 | }
68 | else if loading == true {
69 | carDescription = "Loading..."
70 | engineDescription = ""
71 | }
72 | else {
73 | carDescription = "Unknown"
74 | engineDescription = ""
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Demo/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 | UIInterfaceOrientationPortraitUpsideDown
39 |
40 | UISupportedInterfaceOrientations~ipad
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationPortraitUpsideDown
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Demo/DemoTests/DemoTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoTests.swift
3 | // DemoTests
4 | //
5 | // Created by Dal Rupnik on 23/05/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Demo
11 |
12 | class DemoTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Demo/DemoTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Demo/ViewModelable/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Demo/ViewModelable/ViewModelable.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelable.h
3 | // ViewModelable
4 | //
5 | // Created by Dal Rupnik on 23/11/2016.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for ViewModelable.
12 | FOUNDATION_EXPORT double ViewModelableVersionNumber;
13 |
14 | //! Project version string for ViewModelable.
15 | FOUNDATION_EXPORT const unsigned char ViewModelableVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Demo/ViewModelableTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Demo/ViewModelableTests/ViewModelableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelableTests.swift
3 | // ViewModelableTests
4 | //
5 | // Created by Dal Rupnik on 23/11/2016.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ViewModelable
11 |
12 | class ViewModelableTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Unified Sense
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "ViewModelable",
6 | platforms: [
7 | .iOS(.v15)
8 | ],
9 | products: [
10 | .library(name: "ViewModelable", targets: ["ViewModelable"]),
11 | ],
12 | targets: [
13 | .target(
14 | name: "ViewModelable",
15 | path: "ViewModelable",
16 | swiftSettings: [
17 | .swiftLanguageMode(.v6)
18 | ]
19 | )
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ViewModelable
2 |
3 | ViewModelable is a simple and lightweight **Model View ViewModel** pattern implementation in Swift without any external dependencies for iOS. MVVM pattern is usually used with Reactive Extensions for bindings, but not always. View Model serves to separate any non-UI code away from View Controllers in this case. Each `UIViewController` is backed by a single instance of corresponding view model. Business logic in view model should always be able to create the initial state, regardless of the device state (even without an internet connection, the view model should be valid).
4 |
5 | Each View Model should have input variables and output variables, which get populated based on input or local cache. Output variables usually should not be optional.
6 |
7 | **The idea is that all outputs on `ViewModel` are always defined and initialized for `UIViewController` to be displayed, independent of device state, network connection or similar error states.** It also allows of easy unit testing for the view models.
8 |
9 | View Model has 5 states:
10 |
11 | - Initialized
12 | - Setuped
13 | - Loading
14 | - Loaded
15 | - Unloading
16 |
17 | The state changes follow the next path:
18 |
19 | Initialized -> Setuped -> Loading -> Loaded -> Updates -> Unloading -> Setuped*
20 |
21 | Loaded state can receive multiple callbacks, but the ViewModel cannot proceed back to initialized state.
22 |
23 | View Model informs the view controller of state changes via observable pattern (similar to delegation). The methods received by the observer:
24 |
25 | ```swift
26 | func viewModelDidSetup (viewModel: ViewModel)
27 | func viewModelWillLoad (viewModel: ViewModel)
28 | func viewModelDidLoad (viewModel: ViewModel)
29 | func viewModelDidUpdate (viewModel: ViewModel, updates: [String : AnyObject])
30 | func viewModelWillUnload (viewModel: ViewModel)
31 | func viewModelDidUnload (viewModel: ViewModel)
32 | ```
33 |
34 | You can also check view model's state with using `.state` property. State transitions are asynchronous, because view model usually works with async operations. The callback does not necessarily happen on main thread, so be sure to use dispatch correctly.
35 |
36 | # Installation
37 |
38 | Add `https://github.com/legoless/ViewModelable` as package to your project using Swift Package Manager.
39 |
40 | # Example
41 |
42 | The example below implements a simple view model for a car, without usage of a specific model object.
43 |
44 | ```swift
45 | import ViewModelable
46 |
47 | @MainActor
48 | class CarViewModel : ViewModel {
49 | // MARK: Input
50 | var make : String?
51 | var model : String?
52 |
53 | // MARK: Output
54 | private(set) var horsePower : Double = 0.0
55 | private(set) var weight : Double = 0.0
56 |
57 | func updateOutput() {
58 | guard let make = make, model = model else {
59 | horsePower = 0.0
60 | weight = 0.0
61 |
62 | return
63 | }
64 | if make == "Lamborgihini" && model == "Huracan" {
65 | horsePower = 782
66 | weight = 2100
67 | }
68 | else {
69 | horsePower = 120
70 | weight = 1100
71 | }
72 | }
73 | }
74 | ```
75 |
76 | A simple view controller implementation for the view model.
77 |
78 | ```swift
79 | @MainActor
80 | class CarViewController : ModelableViewController, ViewModelObservable {
81 | //
82 | // MARK: ViewModelObservable
83 | //
84 | func viewModelDidLoad (viewModel: ViewModel) {
85 | //
86 | // Update screen properties
87 | //
88 |
89 | // self.textLabel.text = "\(self.viewModel.horsePower) kW"
90 | }
91 | }
92 |
93 | ```
94 |
95 | Contact
96 | ======
97 |
98 | Dal Rupnik
99 |
100 | - [legoless](https://github.com/legoless) on **GitHub**
101 | - [@thelegoless](https://twitter.com/thelegoless) on **Twitter**
102 | - [dal@unifiedsense.com](mailto:dal@unifiedsense.com)
103 |
104 | License
105 | ======
106 |
107 | **ViewModelable** is available under the **MIT** license. See [LICENSE](https://github.com/Legoless/ViewModelable/blob/master/LICENSE) file for more information.
108 |
--------------------------------------------------------------------------------
/ViewModelable/ModelableCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelableCollectionViewController.swift
3 | // ViewModelable
4 | //
5 | // Created by Dal Rupnik on 29/08/2017.
6 | // Copyright © 2017 Unified Sense. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @MainActor
12 | open class ModelableCollectionViewController : UICollectionViewController, ViewModelObservable {
13 | public var viewModel : T = T()
14 |
15 | deinit {
16 | NotificationCenter.default.removeObserver(self)
17 | }
18 |
19 | open override func viewDidLoad() {
20 | super.viewDidLoad()
21 |
22 | viewModel.observer = self
23 |
24 | for childViewModel in viewModel.childViewModels {
25 | childViewModel.observer = self
26 | }
27 |
28 | viewModel.setup()
29 | }
30 |
31 | open override func viewWillAppear(_ animated: Bool) {
32 | super.viewWillAppear(animated)
33 |
34 | viewModel.load()
35 | }
36 |
37 | open override func viewWillDisappear(_ animated: Bool) {
38 | super.viewWillDisappear(animated)
39 |
40 | viewModel.unload()
41 | }
42 |
43 | public func viewModelDidSetup (_ viewModel: ViewModel) {
44 | guard let viewModel = viewModel as? T else {
45 | return
46 | }
47 | viewModelDidSetup(viewModel: viewModel)
48 | }
49 |
50 | public func viewModelWillLoad (_ viewModel: ViewModel) {
51 | guard let viewModel = viewModel as? T else {
52 | return
53 | }
54 | viewModelWillLoad(viewModel: viewModel)
55 | }
56 |
57 | public func viewModelDidLoad (_ viewModel: ViewModel) {
58 | guard let viewModel = viewModel as? T else {
59 | return
60 | }
61 | viewModelDidLoad(viewModel: viewModel)
62 | }
63 |
64 | public func viewModelDidUpdate (_ viewModel: ViewModel, updates: [String : Any]) {
65 | guard let viewModel = viewModel as? T else {
66 | return
67 | }
68 | viewModelDidUpdate(viewModel: viewModel, updates: updates)
69 | }
70 |
71 | public func viewModelWillUnload (_ viewModel: ViewModel) {
72 | guard let viewModel = viewModel as? T else {
73 | return
74 | }
75 | viewModelWillUnload(viewModel: viewModel)
76 | }
77 |
78 | public func viewModelDidUnload (_ viewModel: ViewModel) {
79 | guard let viewModel = viewModel as? T else {
80 | return
81 | }
82 | viewModelDidUnload(viewModel: viewModel)
83 | }
84 |
85 | open func viewModelDidSetup (viewModel: T) {
86 |
87 | }
88 |
89 | open func viewModelWillLoad (viewModel: T) {
90 |
91 | }
92 |
93 | open func viewModelDidLoad (viewModel: T) {
94 |
95 | }
96 |
97 | open func viewModelDidUpdate (viewModel: T, updates: [String : Any]) {
98 |
99 | }
100 |
101 | open func viewModelWillUnload (viewModel: T) {
102 |
103 | }
104 |
105 | open func viewModelDidUnload (viewModel: T) {
106 |
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/ViewModelable/ModelableTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelableTableViewController.swift
3 | // Demo
4 | //
5 | // Created by Dal Rupnik on 29/08/2017.
6 | // Copyright © 2017 Unified Sense. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | open class ModelableTableViewController : UITableViewController, ViewModelObservable {
12 | public var viewModel : T = T()
13 |
14 | deinit {
15 | NotificationCenter.default.removeObserver(self)
16 | }
17 |
18 | open override func viewDidLoad() {
19 | super.viewDidLoad()
20 |
21 | viewModel.observer = self
22 |
23 | for childViewModel in viewModel.childViewModels {
24 | childViewModel.observer = self
25 | }
26 |
27 | viewModel.setup()
28 | }
29 |
30 | open override func viewWillAppear(_ animated: Bool) {
31 | super.viewWillAppear(animated)
32 |
33 | viewModel.load()
34 | }
35 |
36 | open override func viewWillDisappear(_ animated: Bool) {
37 | super.viewWillDisappear(animated)
38 |
39 | viewModel.unload()
40 | }
41 |
42 | public func viewModelDidSetup (_ viewModel: ViewModel) {
43 | guard let viewModel = viewModel as? T else {
44 | return
45 | }
46 | viewModelDidSetup(viewModel: viewModel)
47 | }
48 |
49 | public func viewModelWillLoad (_ viewModel: ViewModel) {
50 | guard let viewModel = viewModel as? T else {
51 | return
52 | }
53 | viewModelWillLoad(viewModel: viewModel)
54 | }
55 |
56 | public func viewModelDidLoad (_ viewModel: ViewModel) {
57 | guard let viewModel = viewModel as? T else {
58 | return
59 | }
60 | viewModelDidLoad(viewModel: viewModel)
61 | }
62 |
63 | public func viewModelDidUpdate (_ viewModel: ViewModel, updates: [String : Any]) {
64 | guard let viewModel = viewModel as? T else {
65 | return
66 | }
67 | viewModelDidUpdate(viewModel: viewModel, updates: updates)
68 | }
69 |
70 | public func viewModelWillUnload (_ viewModel: ViewModel) {
71 | guard let viewModel = viewModel as? T else {
72 | return
73 | }
74 | viewModelWillUnload(viewModel: viewModel)
75 | }
76 |
77 | public func viewModelDidUnload (_ viewModel: ViewModel) {
78 | guard let viewModel = viewModel as? T else {
79 | return
80 | }
81 | viewModelDidUnload(viewModel: viewModel)
82 | }
83 |
84 | open func viewModelDidSetup (viewModel: T) {
85 |
86 | }
87 |
88 | open func viewModelWillLoad (viewModel: T) {
89 |
90 | }
91 |
92 | open func viewModelDidLoad (viewModel: T) {
93 |
94 | }
95 |
96 | open func viewModelDidUpdate (viewModel: T, updates: [String : Any]) {
97 |
98 | }
99 |
100 | open func viewModelWillUnload (viewModel: T) {
101 |
102 | }
103 |
104 | open func viewModelDidUnload (viewModel: T) {
105 |
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/ViewModelable/ModelableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelableViewController.swift
3 | // ViewModelable
4 | //
5 | // Created by Dal Rupnik on 06/06/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | open class ModelableViewController : UIViewController, ViewModelObservable {
12 | public var viewModel : T = T()
13 |
14 | deinit {
15 | NotificationCenter.default.removeObserver(self)
16 | }
17 |
18 | open override func viewDidLoad() {
19 | super.viewDidLoad()
20 |
21 | viewModel.observer = self
22 |
23 | for childViewModel in viewModel.childViewModels {
24 | childViewModel.observer = self
25 | }
26 |
27 | viewModel.setup()
28 | }
29 |
30 | open override func viewWillAppear(_ animated: Bool) {
31 | super.viewWillAppear(animated)
32 |
33 | viewModel.load()
34 | }
35 |
36 | open override func viewWillDisappear(_ animated: Bool) {
37 | super.viewWillDisappear(animated)
38 |
39 | viewModel.unload()
40 | }
41 |
42 | public func viewModelDidSetup (_ viewModel: ViewModel) {
43 | guard let viewModel = viewModel as? T else {
44 | return
45 | }
46 | viewModelDidSetup(viewModel: viewModel)
47 | }
48 |
49 | public func viewModelWillLoad (_ viewModel: ViewModel) {
50 | guard let viewModel = viewModel as? T else {
51 | return
52 | }
53 | viewModelWillLoad(viewModel: viewModel)
54 | }
55 |
56 | public func viewModelDidLoad (_ viewModel: ViewModel) {
57 | guard let viewModel = viewModel as? T else {
58 | return
59 | }
60 | viewModelDidLoad(viewModel: viewModel)
61 | }
62 |
63 | public func viewModelDidUpdate (_ viewModel: ViewModel, updates: [String : Any]) {
64 | guard let viewModel = viewModel as? T else {
65 | return
66 | }
67 | viewModelDidUpdate(viewModel: viewModel, updates: updates)
68 | }
69 |
70 | public func viewModelWillUnload (_ viewModel: ViewModel) {
71 | guard let viewModel = viewModel as? T else {
72 | return
73 | }
74 | viewModelWillUnload(viewModel: viewModel)
75 | }
76 |
77 | public func viewModelDidUnload (_ viewModel: ViewModel) {
78 | guard let viewModel = viewModel as? T else {
79 | return
80 | }
81 | viewModelDidUnload(viewModel: viewModel)
82 | }
83 |
84 | open func viewModelDidSetup (viewModel: T) {
85 |
86 | }
87 |
88 | open func viewModelWillLoad (viewModel: T) {
89 |
90 | }
91 |
92 | open func viewModelDidLoad (viewModel: T) {
93 |
94 | }
95 |
96 | open func viewModelDidUpdate (viewModel: T, updates: [String : Any]) {
97 |
98 | }
99 |
100 | open func viewModelWillUnload (viewModel: T) {
101 |
102 | }
103 |
104 | open func viewModelDidUnload (viewModel: T) {
105 |
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/ViewModelable/ViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModel.swift
3 | // ViewModelable
4 | //
5 | // Created by Dal Rupnik on 04/03/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | ///
12 | /// View Model state representation
13 | ///
14 | public enum State : UInt {
15 | case initialized // View Model was initialized (first state). Setup should be called, before output is available.
16 | case setuped // View Model was setuped. Setup was called, but data was not loaded yet. Output is should be available.
17 | case loading // View Model is currently refreshing data, offline data should be available.
18 | case loaded // View Model is loaded and subscribed, it will emit updates.
19 | case unloading // View Model was unloaded and will transition to Setuped state.
20 | }
21 |
22 | ///
23 | /// Abstract Generic View model object, abstract
24 | ///
25 | @MainActor
26 | open class ViewModel: NSObject {
27 |
28 | //
29 | // MARK: Private Properties
30 | //
31 |
32 | //
33 | // MARK: Public Properties
34 | //
35 |
36 | // If ViewModel should update it's output on loading states.
37 | public var updateOutputOnLoad = true
38 |
39 | public private(set) weak var parent: ViewModel?
40 |
41 | // Observer of view model, usually a controller or a view
42 | public weak var observer : ViewModelObservable?
43 |
44 | // When view model was initialized
45 | public fileprivate(set) var initializationDate = Date()
46 |
47 | // When view model was loaded
48 | public fileprivate(set) var loadDate : Date?
49 |
50 | public fileprivate(set) var state = State.initialized
51 |
52 | //
53 | // Child view models that are contained
54 | //
55 | public private(set) var childViewModels : [ViewModel] = [ViewModel]()
56 |
57 | //
58 | // MARK: Initialization
59 | //
60 | public required override init() {
61 | super.init()
62 | }
63 |
64 | deinit {
65 | NotificationCenter.default.removeObserver(self)
66 | }
67 |
68 | //
69 | // MARK: Public API for interacting with view model, only call these methods outside View Model.
70 | //
71 |
72 | /*!
73 | Should be called in View Did Load, this will setup the defaults and after this state, all input variables
74 | should be prepared. Input variables usually should not have Live Realm objects, but their snapshots.
75 | */
76 | public final func setup () {
77 | if state != .initialized {
78 | return
79 | }
80 |
81 | state = .setuped
82 |
83 | startSetup()
84 | updateOutput()
85 |
86 | //
87 | // Setup child view models
88 | //
89 |
90 | for viewModel in childViewModels {
91 | viewModel.setup()
92 | }
93 |
94 | if let observer = observer {
95 | observer.viewModelDidSetup(self)
96 | }
97 | }
98 |
99 | /*!
100 | Begins loading view model, starting by root subscription, should be called in view did appear.
101 | */
102 | public final func load() {
103 | if state != .setuped {
104 | return
105 | }
106 |
107 | state = .loading
108 |
109 | //
110 | // Update output to ensure loading state.
111 | //
112 |
113 | if let observer = observer {
114 | observer.viewModelWillLoad(self)
115 | }
116 |
117 | //
118 | // Load child view models
119 | //
120 |
121 | for viewModel in childViewModels {
122 | viewModel.load()
123 | }
124 |
125 | startLoading()
126 | }
127 |
128 | //
129 | // MARK: Public Methods
130 | //
131 |
132 | public final func addChild(viewModel: ViewModel) {
133 | if (childViewModels.firstIndex(where: { $0 === viewModel }) != nil) {
134 | return
135 | }
136 |
137 | childViewModels.append(viewModel)
138 |
139 | viewModel.parent = self
140 | }
141 |
142 | public final func removeChild(viewModel: ViewModel) {
143 | guard let index = childViewModels.firstIndex(where: { $0 === viewModel }) else {
144 | return
145 | }
146 |
147 | viewModel.parent = nil
148 |
149 | childViewModels.remove(at: index)
150 | }
151 |
152 | public final func removeFromParent() {
153 | guard let parent = parent else {
154 | return
155 | }
156 |
157 | parent.removeChild(viewModel: self)
158 | }
159 |
160 | /*!
161 | Should be called when view disappears, this will clean state of view model.
162 | */
163 | public final func unload() {
164 | if state != .loaded {
165 | return
166 | }
167 |
168 | state = .unloading
169 |
170 | if let observer = observer {
171 | observer.viewModelWillUnload(self)
172 | }
173 |
174 | //
175 | // Load child view models
176 | //
177 |
178 | for viewModel in childViewModels {
179 | viewModel.unload()
180 | }
181 |
182 | startUnloading()
183 | }
184 |
185 | //
186 | // Finish loading must be called by a subclass when loading is completed, so the view model state is correctly set.
187 | // Observer is notified of model successfully loading. Output variables are reset to ensure the most correct state.
188 | //
189 | public final func finishLoading() {
190 |
191 | state = .loaded
192 | loadDate = Date()
193 |
194 | if updateOutputOnLoad {
195 | updateOutput()
196 | }
197 |
198 | if let observer = self.observer {
199 | observer.viewModelDidLoad(self)
200 | }
201 | }
202 |
203 | /*!
204 | Finish unloading must be called by a subclass to correctly transition back into Setuped state.
205 | */
206 | public final func finishUnloading() {
207 |
208 | state = .setuped
209 | loadDate = nil
210 |
211 | if updateOutputOnLoad {
212 | updateOutput()
213 | }
214 |
215 | if let observer = observer {
216 | observer.viewModelDidUnload(self)
217 | }
218 | }
219 |
220 | //
221 | // MARK: Public Methods that should be overriden by subclass for correct life-cycle.
222 | //
223 |
224 | /*!
225 | Should be overriden, if there are any input defaults that should be set by subclass as the setup
226 | of the view model.
227 | */
228 | open func startSetup () {
229 |
230 | }
231 |
232 | /*!
233 | Must be overriden by a subclass to correctly start loading output variables, if view model is
234 | doing custom loading. Subclass should not call super's startLoading, unless specifically required.
235 | */
236 | open func startLoading() {
237 | finishLoading()
238 | }
239 |
240 | /*!
241 | Must be overriden by a subclass to correctly start unloading output variables. This is to clean
242 | Memory up if the corresponding view controller is not on screen.
243 | */
244 | open func startUnloading() {
245 | finishUnloading()
246 | }
247 |
248 | /*!
249 | Must be overriden by a subclass to correctly update output. This method should take any input
250 | and provide output variables.
251 | */
252 | open func updateOutput() {
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/ViewModelable/ViewModelObservable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelObservable.swift
3 | // ViewModelable
4 | //
5 | // Created by Dal Rupnik on 20/04/16.
6 | // Copyright © 2016 Unified Sense. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /*!
12 | * Both methods are not guaranteed to be called back on the same thread as requested,
13 | * so be sure to use a dispatch when necessary.
14 | */
15 | @MainActor
16 | public protocol ViewModelObservable : AnyObject {
17 |
18 | /*!
19 | Called after model is successfully initialized with input data.
20 |
21 | - parameter viewModel: setuped view model
22 | */
23 | func viewModelDidSetup (_ viewModel: ViewModel)
24 |
25 | /*!
26 | Called after calling setup method, when root subscriptions of the view model begin loading.
27 | View model should be in offline state, when this method is called, so it is safe to
28 | display objects from View Model. Usually entire view controller should be reloaded at this point.
29 |
30 | All available output variables will be set up, but could change when model loads.
31 |
32 | - parameter viewModel: view model to be loaded
33 | */
34 | func viewModelWillLoad (_ viewModel: ViewModel)
35 |
36 | /*!
37 | Called when view model has finished loading and all output variables are available
38 | to be displayed. This method does not ensure all child view models had finished loading,
39 | as those models should be specifically observed. It can be called multiple times.
40 |
41 | - parameter viewModel: loaded view model
42 | */
43 | func viewModelDidLoad (_ viewModel: ViewModel)
44 |
45 | /*!
46 | Call to observer when view model updated a part of data (not entire set). The method can
47 | be called multiple times.
48 |
49 | - parameter viewModel: updated view model
50 | */
51 | func viewModelDidUpdate (_ viewModel: ViewModel, updates: [String : Any])
52 |
53 | /*!
54 | View model will transition back from Loaded state to Setuped state, since unload was called.
55 | Subscriptions and observers will be removed after this.
56 |
57 | - parameter viewModel: view model to be unloaded
58 | */
59 | func viewModelWillUnload (_ viewModel: ViewModel)
60 |
61 | /*!
62 | View model transitioned back to setuped state and objects are not available anymore.
63 |
64 | - parameter viewModel: unloaded view model
65 | */
66 | func viewModelDidUnload (_ viewModel: ViewModel)
67 | }
68 |
69 | //
70 | // MARK: Default implementations, so they are optional.
71 | //
72 |
73 | @MainActor
74 | extension ViewModelObservable {
75 |
76 | public func viewModelDidSetup (_ viewModel: ViewModel) {
77 |
78 | }
79 |
80 | public func viewModelWillLoad (_ viewModel: ViewModel) {
81 |
82 | }
83 |
84 | public func viewModelDidLoad (_ viewModel: ViewModel) {
85 |
86 | }
87 |
88 | public func viewModelDidUpdate (_ viewModel: ViewModel, updates: [String : Any]) {
89 |
90 | }
91 |
92 | public func viewModelWillUnload (_ viewModel: ViewModel) {
93 |
94 | }
95 |
96 | public func viewModelDidUnload (_ viewModel: ViewModel) {
97 |
98 | }
99 | }
100 |
--------------------------------------------------------------------------------