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