├── MVVMC.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── MVVMC ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Group │ ├── Coordinator │ │ └── GroupCoordinator.swift │ ├── Interactor │ │ └── GroupInteractor.swift │ ├── View │ │ └── GroupViewController.swift │ └── ViewModel │ │ └── GroupViewModel.swift ├── Groups │ ├── Coordinator │ │ ├── GroupsCoordinator.swift │ │ └── RootCoordinator.swift │ ├── Interactor │ │ └── GroupsInteractor.swift │ ├── Model │ │ └── GroupsList.swift │ ├── View │ │ ├── GroupTableViewCell.swift │ │ └── GroupsViewController.swift │ └── ViewModel │ │ ├── GroupCellViewModel.swift │ │ └── GroupsViewModel.swift ├── Info.plist ├── Modules │ └── GroupDataProvider.swift ├── ObserverSet │ └── ObserverSet.swift └── Resources │ └── Default-568h@2x.png ├── Readme.md └── UnitTests ├── GroupsViewModelTests.swift └── Info.plist /MVVMC.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2823F48E1F33538B00378EE4 /* GroupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F48D1F33538B00378EE4 /* GroupCoordinator.swift */; }; 11 | 2823F4901F33541D00378EE4 /* GroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F48F1F33541D00378EE4 /* GroupInteractor.swift */; }; 12 | 2823F4921F33545000378EE4 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F4911F33545000378EE4 /* GroupViewModel.swift */; }; 13 | 2823F4941F33547300378EE4 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F4931F33547300378EE4 /* GroupViewController.swift */; }; 14 | 28311E861F2B522B00CA5406 /* GroupsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E851F2B522B00CA5406 /* GroupsList.swift */; }; 15 | 28311E881F2B53AF00CA5406 /* GroupsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E871F2B53AF00CA5406 /* GroupsViewController.swift */; }; 16 | 28311E8A1F2B53D600CA5406 /* GroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E891F2B53D600CA5406 /* GroupsViewModel.swift */; }; 17 | 28311E8C1F2B544300CA5406 /* GroupsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E8B1F2B544300CA5406 /* GroupsCoordinator.swift */; }; 18 | 28311E8E1F2B546400CA5406 /* GroupsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E8D1F2B546400CA5406 /* GroupsInteractor.swift */; }; 19 | 2856304C1F33578D0075A78F /* GroupsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2856304B1F33578D0075A78F /* GroupsViewModelTests.swift */; }; 20 | 285630531F3358A90075A78F /* GroupsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E8D1F2B546400CA5406 /* GroupsInteractor.swift */; }; 21 | 285630541F3358AD0075A78F /* GroupsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E851F2B522B00CA5406 /* GroupsList.swift */; }; 22 | 285630551F3358B00075A78F /* GroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E891F2B53D600CA5406 /* GroupsViewModel.swift */; }; 23 | 285630561F3358B20075A78F /* GroupsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E8B1F2B544300CA5406 /* GroupsCoordinator.swift */; }; 24 | 285630581F3358DE0075A78F /* GroupDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285630571F3358DE0075A78F /* GroupDataProvider.swift */; }; 25 | 2856305C1F335A070075A78F /* ObserverSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2856305B1F335A070075A78F /* ObserverSet.swift */; }; 26 | 285D50E11F335C8C0017A9C8 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F4931F33547300378EE4 /* GroupViewController.swift */; }; 27 | 285D50E21F335C8C0017A9C8 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F4911F33545000378EE4 /* GroupViewModel.swift */; }; 28 | 285D50E31F335C8C0017A9C8 /* GroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F48F1F33541D00378EE4 /* GroupInteractor.swift */; }; 29 | 285D50E41F335C8C0017A9C8 /* GroupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2823F48D1F33538B00378EE4 /* GroupCoordinator.swift */; }; 30 | 285D50E51F335C8C0017A9C8 /* GroupsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28311E871F2B53AF00CA5406 /* GroupsViewController.swift */; }; 31 | 285D50E61F335C8C0017A9C8 /* GroupDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285630571F3358DE0075A78F /* GroupDataProvider.swift */; }; 32 | 285D50E71F335C8C0017A9C8 /* ObserverSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2856305B1F335A070075A78F /* ObserverSet.swift */; }; 33 | 285D50E91F335D560017A9C8 /* RootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285D50E81F335D560017A9C8 /* RootCoordinator.swift */; }; 34 | 286B7B761F2B4FEF004877C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 286B7B751F2B4FEF004877C2 /* AppDelegate.swift */; }; 35 | 286B7B7D1F2B4FEF004877C2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 286B7B7C1F2B4FEF004877C2 /* Assets.xcassets */; }; 36 | 286DE1D41F3B466A00ABA09A /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 286DE1D31F3B466A00ABA09A /* Default-568h@2x.png */; }; 37 | 28863CF01F34AEE50087BAD4 /* GroupCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28863CEF1F34AEE50087BAD4 /* GroupCellViewModel.swift */; }; 38 | 28863CF21F34AF310087BAD4 /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28863CF11F34AF310087BAD4 /* GroupTableViewCell.swift */; }; 39 | 28863CF51F3854E10087BAD4 /* GroupCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28863CEF1F34AEE50087BAD4 /* GroupCellViewModel.swift */; }; 40 | 28863CF61F3855250087BAD4 /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28863CF11F34AF310087BAD4 /* GroupTableViewCell.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | 2856304E1F33578D0075A78F /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 286B7B6A1F2B4FEF004877C2 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 286B7B711F2B4FEF004877C2; 49 | remoteInfo = MVVMC; 50 | }; 51 | /* End PBXContainerItemProxy section */ 52 | 53 | /* Begin PBXFileReference section */ 54 | 2823F48D1F33538B00378EE4 /* GroupCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCoordinator.swift; sourceTree = ""; }; 55 | 2823F48F1F33541D00378EE4 /* GroupInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInteractor.swift; sourceTree = ""; }; 56 | 2823F4911F33545000378EE4 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = ""; }; 57 | 2823F4931F33547300378EE4 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = ""; }; 58 | 28311E851F2B522B00CA5406 /* GroupsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsList.swift; sourceTree = ""; }; 59 | 28311E871F2B53AF00CA5406 /* GroupsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsViewController.swift; sourceTree = ""; }; 60 | 28311E891F2B53D600CA5406 /* GroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsViewModel.swift; sourceTree = ""; }; 61 | 28311E8B1F2B544300CA5406 /* GroupsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsCoordinator.swift; sourceTree = ""; }; 62 | 28311E8D1F2B546400CA5406 /* GroupsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsInteractor.swift; sourceTree = ""; }; 63 | 285630491F33578D0075A78F /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 2856304B1F33578D0075A78F /* GroupsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsViewModelTests.swift; sourceTree = ""; }; 65 | 2856304D1F33578D0075A78F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 285630571F3358DE0075A78F /* GroupDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDataProvider.swift; sourceTree = ""; }; 67 | 2856305B1F335A070075A78F /* ObserverSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverSet.swift; sourceTree = ""; }; 68 | 285D50E81F335D560017A9C8 /* RootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCoordinator.swift; sourceTree = ""; }; 69 | 286B7B721F2B4FEF004877C2 /* MVVMC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MVVMC.app; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 286B7B751F2B4FEF004877C2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 71 | 286B7B7C1F2B4FEF004877C2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 286B7B811F2B4FEF004877C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 286DE1D31F3B466A00ABA09A /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 74 | 28863CEF1F34AEE50087BAD4 /* GroupCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCellViewModel.swift; sourceTree = ""; }; 75 | 28863CF11F34AF310087BAD4 /* GroupTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = ""; }; 76 | 28863D741F3B401E0087BAD4 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 285630461F33578D0075A78F /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | 286B7B6F1F2B4FEF004877C2 /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | 2823F4891F3352F800378EE4 /* Coordinator */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 2823F48D1F33538B00378EE4 /* GroupCoordinator.swift */, 101 | ); 102 | path = Coordinator; 103 | sourceTree = ""; 104 | }; 105 | 2823F48A1F33530F00378EE4 /* Interactor */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 2823F48F1F33541D00378EE4 /* GroupInteractor.swift */, 109 | ); 110 | path = Interactor; 111 | sourceTree = ""; 112 | }; 113 | 2823F48B1F33531600378EE4 /* ViewModel */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 2823F4911F33545000378EE4 /* GroupViewModel.swift */, 117 | ); 118 | path = ViewModel; 119 | sourceTree = ""; 120 | }; 121 | 2823F48C1F33534700378EE4 /* View */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 2823F4931F33547300378EE4 /* GroupViewController.swift */, 125 | ); 126 | path = View; 127 | sourceTree = ""; 128 | }; 129 | 28311E7F1F2B516100CA5406 /* Groups */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 28311E841F2B519900CA5406 /* Coordinator */, 133 | 28311E831F2B518E00CA5406 /* Interactor */, 134 | 28311E801F2B517200CA5406 /* Model */, 135 | 28311E821F2B518000CA5406 /* View */, 136 | 28311E811F2B517900CA5406 /* ViewModel */, 137 | ); 138 | path = Groups; 139 | sourceTree = ""; 140 | }; 141 | 28311E801F2B517200CA5406 /* Model */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 28311E851F2B522B00CA5406 /* GroupsList.swift */, 145 | ); 146 | path = Model; 147 | sourceTree = ""; 148 | }; 149 | 28311E811F2B517900CA5406 /* ViewModel */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 28863CEF1F34AEE50087BAD4 /* GroupCellViewModel.swift */, 153 | 28311E891F2B53D600CA5406 /* GroupsViewModel.swift */, 154 | ); 155 | path = ViewModel; 156 | sourceTree = ""; 157 | }; 158 | 28311E821F2B518000CA5406 /* View */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 28311E871F2B53AF00CA5406 /* GroupsViewController.swift */, 162 | 28863CF11F34AF310087BAD4 /* GroupTableViewCell.swift */, 163 | ); 164 | path = View; 165 | sourceTree = ""; 166 | }; 167 | 28311E831F2B518E00CA5406 /* Interactor */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 28311E8D1F2B546400CA5406 /* GroupsInteractor.swift */, 171 | ); 172 | path = Interactor; 173 | sourceTree = ""; 174 | }; 175 | 28311E841F2B519900CA5406 /* Coordinator */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 28311E8B1F2B544300CA5406 /* GroupsCoordinator.swift */, 179 | 285D50E81F335D560017A9C8 /* RootCoordinator.swift */, 180 | ); 181 | path = Coordinator; 182 | sourceTree = ""; 183 | }; 184 | 2856304A1F33578D0075A78F /* UnitTests */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 2856304B1F33578D0075A78F /* GroupsViewModelTests.swift */, 188 | 2856304D1F33578D0075A78F /* Info.plist */, 189 | ); 190 | path = UnitTests; 191 | sourceTree = ""; 192 | }; 193 | 285630591F3358E60075A78F /* Modules */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 285630571F3358DE0075A78F /* GroupDataProvider.swift */, 197 | ); 198 | path = Modules; 199 | sourceTree = ""; 200 | }; 201 | 2856305A1F3359F80075A78F /* ObserverSet */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 2856305B1F335A070075A78F /* ObserverSet.swift */, 205 | ); 206 | path = ObserverSet; 207 | sourceTree = ""; 208 | }; 209 | 286B7B691F2B4FEF004877C2 = { 210 | isa = PBXGroup; 211 | children = ( 212 | 28863D741F3B401E0087BAD4 /* Readme.md */, 213 | 286B7B741F2B4FEF004877C2 /* MVVMC */, 214 | 2856304A1F33578D0075A78F /* UnitTests */, 215 | 286B7B731F2B4FEF004877C2 /* Products */, 216 | ); 217 | sourceTree = ""; 218 | }; 219 | 286B7B731F2B4FEF004877C2 /* Products */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 286B7B721F2B4FEF004877C2 /* MVVMC.app */, 223 | 285630491F33578D0075A78F /* UnitTests.xctest */, 224 | ); 225 | name = Products; 226 | sourceTree = ""; 227 | }; 228 | 286B7B741F2B4FEF004877C2 /* MVVMC */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 289D36501F2F981D00BE167D /* Group */, 232 | 28311E7F1F2B516100CA5406 /* Groups */, 233 | 285630591F3358E60075A78F /* Modules */, 234 | 2856305A1F3359F80075A78F /* ObserverSet */, 235 | 286DE1D21F3B466A00ABA09A /* Resources */, 236 | 286B7B751F2B4FEF004877C2 /* AppDelegate.swift */, 237 | 286B7B7C1F2B4FEF004877C2 /* Assets.xcassets */, 238 | 286B7B811F2B4FEF004877C2 /* Info.plist */, 239 | ); 240 | path = MVVMC; 241 | sourceTree = ""; 242 | }; 243 | 286DE1D21F3B466A00ABA09A /* Resources */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | 286DE1D31F3B466A00ABA09A /* Default-568h@2x.png */, 247 | ); 248 | path = Resources; 249 | sourceTree = ""; 250 | }; 251 | 289D36501F2F981D00BE167D /* Group */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | 2823F4891F3352F800378EE4 /* Coordinator */, 255 | 2823F48A1F33530F00378EE4 /* Interactor */, 256 | 2823F48C1F33534700378EE4 /* View */, 257 | 2823F48B1F33531600378EE4 /* ViewModel */, 258 | ); 259 | path = Group; 260 | sourceTree = ""; 261 | }; 262 | /* End PBXGroup section */ 263 | 264 | /* Begin PBXNativeTarget section */ 265 | 285630481F33578D0075A78F /* UnitTests */ = { 266 | isa = PBXNativeTarget; 267 | buildConfigurationList = 285630521F33578D0075A78F /* Build configuration list for PBXNativeTarget "UnitTests" */; 268 | buildPhases = ( 269 | 285630451F33578D0075A78F /* Sources */, 270 | 285630461F33578D0075A78F /* Frameworks */, 271 | 285630471F33578D0075A78F /* Resources */, 272 | ); 273 | buildRules = ( 274 | ); 275 | dependencies = ( 276 | 2856304F1F33578D0075A78F /* PBXTargetDependency */, 277 | ); 278 | name = UnitTests; 279 | productName = UnitTests; 280 | productReference = 285630491F33578D0075A78F /* UnitTests.xctest */; 281 | productType = "com.apple.product-type.bundle.unit-test"; 282 | }; 283 | 286B7B711F2B4FEF004877C2 /* MVVMC */ = { 284 | isa = PBXNativeTarget; 285 | buildConfigurationList = 286B7B841F2B4FEF004877C2 /* Build configuration list for PBXNativeTarget "MVVMC" */; 286 | buildPhases = ( 287 | 286B7B6E1F2B4FEF004877C2 /* Sources */, 288 | 286B7B6F1F2B4FEF004877C2 /* Frameworks */, 289 | 286B7B701F2B4FEF004877C2 /* Resources */, 290 | ); 291 | buildRules = ( 292 | ); 293 | dependencies = ( 294 | ); 295 | name = MVVMC; 296 | productName = MVVMC; 297 | productReference = 286B7B721F2B4FEF004877C2 /* MVVMC.app */; 298 | productType = "com.apple.product-type.application"; 299 | }; 300 | /* End PBXNativeTarget section */ 301 | 302 | /* Begin PBXProject section */ 303 | 286B7B6A1F2B4FEF004877C2 /* Project object */ = { 304 | isa = PBXProject; 305 | attributes = { 306 | LastSwiftUpdateCheck = 0900; 307 | LastUpgradeCheck = 0830; 308 | ORGANIZATIONNAME = runtastic; 309 | TargetAttributes = { 310 | 285630481F33578D0075A78F = { 311 | CreatedOnToolsVersion = 9.0; 312 | DevelopmentTeam = TPLF234WYY; 313 | TestTargetID = 286B7B711F2B4FEF004877C2; 314 | }; 315 | 286B7B711F2B4FEF004877C2 = { 316 | CreatedOnToolsVersion = 8.3.3; 317 | ProvisioningStyle = Automatic; 318 | }; 319 | }; 320 | }; 321 | buildConfigurationList = 286B7B6D1F2B4FEF004877C2 /* Build configuration list for PBXProject "MVVMC" */; 322 | compatibilityVersion = "Xcode 3.2"; 323 | developmentRegion = English; 324 | hasScannedForEncodings = 0; 325 | knownRegions = ( 326 | en, 327 | Base, 328 | ); 329 | mainGroup = 286B7B691F2B4FEF004877C2; 330 | productRefGroup = 286B7B731F2B4FEF004877C2 /* Products */; 331 | projectDirPath = ""; 332 | projectRoot = ""; 333 | targets = ( 334 | 286B7B711F2B4FEF004877C2 /* MVVMC */, 335 | 285630481F33578D0075A78F /* UnitTests */, 336 | ); 337 | }; 338 | /* End PBXProject section */ 339 | 340 | /* Begin PBXResourcesBuildPhase section */ 341 | 285630471F33578D0075A78F /* Resources */ = { 342 | isa = PBXResourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | ); 346 | runOnlyForDeploymentPostprocessing = 0; 347 | }; 348 | 286B7B701F2B4FEF004877C2 /* Resources */ = { 349 | isa = PBXResourcesBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | 286B7B7D1F2B4FEF004877C2 /* Assets.xcassets in Resources */, 353 | 286DE1D41F3B466A00ABA09A /* Default-568h@2x.png in Resources */, 354 | ); 355 | runOnlyForDeploymentPostprocessing = 0; 356 | }; 357 | /* End PBXResourcesBuildPhase section */ 358 | 359 | /* Begin PBXSourcesBuildPhase section */ 360 | 285630451F33578D0075A78F /* Sources */ = { 361 | isa = PBXSourcesBuildPhase; 362 | buildActionMask = 2147483647; 363 | files = ( 364 | 285D50E11F335C8C0017A9C8 /* GroupViewController.swift in Sources */, 365 | 28863CF51F3854E10087BAD4 /* GroupCellViewModel.swift in Sources */, 366 | 285D50E21F335C8C0017A9C8 /* GroupViewModel.swift in Sources */, 367 | 285D50E31F335C8C0017A9C8 /* GroupInteractor.swift in Sources */, 368 | 285D50E41F335C8C0017A9C8 /* GroupCoordinator.swift in Sources */, 369 | 285D50E51F335C8C0017A9C8 /* GroupsViewController.swift in Sources */, 370 | 285D50E61F335C8C0017A9C8 /* GroupDataProvider.swift in Sources */, 371 | 28863CF61F3855250087BAD4 /* GroupTableViewCell.swift in Sources */, 372 | 285D50E71F335C8C0017A9C8 /* ObserverSet.swift in Sources */, 373 | 285630551F3358B00075A78F /* GroupsViewModel.swift in Sources */, 374 | 285630541F3358AD0075A78F /* GroupsList.swift in Sources */, 375 | 285630531F3358A90075A78F /* GroupsInteractor.swift in Sources */, 376 | 285630561F3358B20075A78F /* GroupsCoordinator.swift in Sources */, 377 | 2856304C1F33578D0075A78F /* GroupsViewModelTests.swift in Sources */, 378 | ); 379 | runOnlyForDeploymentPostprocessing = 0; 380 | }; 381 | 286B7B6E1F2B4FEF004877C2 /* Sources */ = { 382 | isa = PBXSourcesBuildPhase; 383 | buildActionMask = 2147483647; 384 | files = ( 385 | 28311E8E1F2B546400CA5406 /* GroupsInteractor.swift in Sources */, 386 | 28863CF01F34AEE50087BAD4 /* GroupCellViewModel.swift in Sources */, 387 | 28311E881F2B53AF00CA5406 /* GroupsViewController.swift in Sources */, 388 | 28863CF21F34AF310087BAD4 /* GroupTableViewCell.swift in Sources */, 389 | 2823F4901F33541D00378EE4 /* GroupInteractor.swift in Sources */, 390 | 28311E8A1F2B53D600CA5406 /* GroupsViewModel.swift in Sources */, 391 | 2823F48E1F33538B00378EE4 /* GroupCoordinator.swift in Sources */, 392 | 28311E8C1F2B544300CA5406 /* GroupsCoordinator.swift in Sources */, 393 | 285D50E91F335D560017A9C8 /* RootCoordinator.swift in Sources */, 394 | 285630581F3358DE0075A78F /* GroupDataProvider.swift in Sources */, 395 | 28311E861F2B522B00CA5406 /* GroupsList.swift in Sources */, 396 | 2823F4941F33547300378EE4 /* GroupViewController.swift in Sources */, 397 | 286B7B761F2B4FEF004877C2 /* AppDelegate.swift in Sources */, 398 | 2823F4921F33545000378EE4 /* GroupViewModel.swift in Sources */, 399 | 2856305C1F335A070075A78F /* ObserverSet.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | /* End PBXSourcesBuildPhase section */ 404 | 405 | /* Begin PBXTargetDependency section */ 406 | 2856304F1F33578D0075A78F /* PBXTargetDependency */ = { 407 | isa = PBXTargetDependency; 408 | target = 286B7B711F2B4FEF004877C2 /* MVVMC */; 409 | targetProxy = 2856304E1F33578D0075A78F /* PBXContainerItemProxy */; 410 | }; 411 | /* End PBXTargetDependency section */ 412 | 413 | /* Begin XCBuildConfiguration section */ 414 | 285630501F33578D0075A78F /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 418 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 419 | CLANG_WARN_COMMA = YES; 420 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 422 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 423 | CLANG_WARN_STRICT_PROTOTYPES = YES; 424 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 425 | CODE_SIGN_IDENTITY = "iPhone Developer"; 426 | DEVELOPMENT_TEAM = TPLF234WYY; 427 | GCC_C_LANGUAGE_STANDARD = gnu11; 428 | INFOPLIST_FILE = UnitTests/Info.plist; 429 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 430 | PRODUCT_BUNDLE_IDENTIFIER = com.runtastic.iphone.UnitTests; 431 | PRODUCT_NAME = "$(TARGET_NAME)"; 432 | TARGETED_DEVICE_FAMILY = "1,2"; 433 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVMC.app/MVVMC"; 434 | }; 435 | name = Debug; 436 | }; 437 | 285630511F33578D0075A78F /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 441 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 442 | CLANG_WARN_COMMA = YES; 443 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 445 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 446 | CLANG_WARN_STRICT_PROTOTYPES = YES; 447 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 448 | CODE_SIGN_IDENTITY = "iPhone Developer"; 449 | DEVELOPMENT_TEAM = TPLF234WYY; 450 | GCC_C_LANGUAGE_STANDARD = gnu11; 451 | INFOPLIST_FILE = UnitTests/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 453 | PRODUCT_BUNDLE_IDENTIFIER = com.runtastic.iphone.UnitTests; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | TARGETED_DEVICE_FAMILY = "1,2"; 456 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVMC.app/MVVMC"; 457 | }; 458 | name = Release; 459 | }; 460 | 286B7B821F2B4FEF004877C2 /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ALWAYS_SEARCH_USER_PATHS = NO; 464 | CLANG_ANALYZER_NONNULL = YES; 465 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 466 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 467 | CLANG_CXX_LIBRARY = "libc++"; 468 | CLANG_ENABLE_MODULES = YES; 469 | CLANG_ENABLE_OBJC_ARC = YES; 470 | CLANG_WARN_BOOL_CONVERSION = YES; 471 | CLANG_WARN_CONSTANT_CONVERSION = YES; 472 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 473 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 474 | CLANG_WARN_EMPTY_BODY = YES; 475 | CLANG_WARN_ENUM_CONVERSION = YES; 476 | CLANG_WARN_INFINITE_RECURSION = YES; 477 | CLANG_WARN_INT_CONVERSION = YES; 478 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 479 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 480 | CLANG_WARN_UNREACHABLE_CODE = YES; 481 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 482 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 483 | COPY_PHASE_STRIP = NO; 484 | DEBUG_INFORMATION_FORMAT = dwarf; 485 | ENABLE_STRICT_OBJC_MSGSEND = YES; 486 | ENABLE_TESTABILITY = YES; 487 | GCC_C_LANGUAGE_STANDARD = gnu99; 488 | GCC_DYNAMIC_NO_PIC = NO; 489 | GCC_NO_COMMON_BLOCKS = YES; 490 | GCC_OPTIMIZATION_LEVEL = 0; 491 | GCC_PREPROCESSOR_DEFINITIONS = ( 492 | "DEBUG=1", 493 | "$(inherited)", 494 | ); 495 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 496 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 497 | GCC_WARN_UNDECLARED_SELECTOR = YES; 498 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 499 | GCC_WARN_UNUSED_FUNCTION = YES; 500 | GCC_WARN_UNUSED_VARIABLE = YES; 501 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 502 | MTL_ENABLE_DEBUG_INFO = YES; 503 | ONLY_ACTIVE_ARCH = YES; 504 | SDKROOT = iphoneos; 505 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 506 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 507 | SWIFT_VERSION = 3.0; 508 | TARGETED_DEVICE_FAMILY = "1,2"; 509 | }; 510 | name = Debug; 511 | }; 512 | 286B7B831F2B4FEF004877C2 /* Release */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ALWAYS_SEARCH_USER_PATHS = NO; 516 | CLANG_ANALYZER_NONNULL = YES; 517 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 518 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 519 | CLANG_CXX_LIBRARY = "libc++"; 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_ENABLE_OBJC_ARC = YES; 522 | CLANG_WARN_BOOL_CONVERSION = YES; 523 | CLANG_WARN_CONSTANT_CONVERSION = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 526 | CLANG_WARN_EMPTY_BODY = YES; 527 | CLANG_WARN_ENUM_CONVERSION = YES; 528 | CLANG_WARN_INFINITE_RECURSION = YES; 529 | CLANG_WARN_INT_CONVERSION = YES; 530 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 531 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 532 | CLANG_WARN_UNREACHABLE_CODE = YES; 533 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 534 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 535 | COPY_PHASE_STRIP = NO; 536 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 537 | ENABLE_NS_ASSERTIONS = NO; 538 | ENABLE_STRICT_OBJC_MSGSEND = YES; 539 | GCC_C_LANGUAGE_STANDARD = gnu99; 540 | GCC_NO_COMMON_BLOCKS = YES; 541 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 542 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 543 | GCC_WARN_UNDECLARED_SELECTOR = YES; 544 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 545 | GCC_WARN_UNUSED_FUNCTION = YES; 546 | GCC_WARN_UNUSED_VARIABLE = YES; 547 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 548 | MTL_ENABLE_DEBUG_INFO = NO; 549 | SDKROOT = iphoneos; 550 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 551 | SWIFT_VERSION = 3.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | VALIDATE_PRODUCT = YES; 554 | }; 555 | name = Release; 556 | }; 557 | 286B7B851F2B4FEF004877C2 /* Debug */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 561 | DEVELOPMENT_TEAM = ""; 562 | INFOPLIST_FILE = MVVMC/Info.plist; 563 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 564 | PRODUCT_BUNDLE_IDENTIFIER = com.runtastic.iphone.MVVMC; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | }; 567 | name = Debug; 568 | }; 569 | 286B7B861F2B4FEF004877C2 /* Release */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 573 | DEVELOPMENT_TEAM = ""; 574 | INFOPLIST_FILE = MVVMC/Info.plist; 575 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 576 | PRODUCT_BUNDLE_IDENTIFIER = com.runtastic.iphone.MVVMC; 577 | PRODUCT_NAME = "$(TARGET_NAME)"; 578 | }; 579 | name = Release; 580 | }; 581 | /* End XCBuildConfiguration section */ 582 | 583 | /* Begin XCConfigurationList section */ 584 | 285630521F33578D0075A78F /* Build configuration list for PBXNativeTarget "UnitTests" */ = { 585 | isa = XCConfigurationList; 586 | buildConfigurations = ( 587 | 285630501F33578D0075A78F /* Debug */, 588 | 285630511F33578D0075A78F /* Release */, 589 | ); 590 | defaultConfigurationIsVisible = 0; 591 | defaultConfigurationName = Release; 592 | }; 593 | 286B7B6D1F2B4FEF004877C2 /* Build configuration list for PBXProject "MVVMC" */ = { 594 | isa = XCConfigurationList; 595 | buildConfigurations = ( 596 | 286B7B821F2B4FEF004877C2 /* Debug */, 597 | 286B7B831F2B4FEF004877C2 /* Release */, 598 | ); 599 | defaultConfigurationIsVisible = 0; 600 | defaultConfigurationName = Release; 601 | }; 602 | 286B7B841F2B4FEF004877C2 /* Build configuration list for PBXNativeTarget "MVVMC" */ = { 603 | isa = XCConfigurationList; 604 | buildConfigurations = ( 605 | 286B7B851F2B4FEF004877C2 /* Debug */, 606 | 286B7B861F2B4FEF004877C2 /* Release */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | /* End XCConfigurationList section */ 612 | }; 613 | rootObject = 286B7B6A1F2B4FEF004877C2 /* Project object */; 614 | } 615 | -------------------------------------------------------------------------------- /MVVMC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVVMC/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.backgroundColor = UIColor.white 20 | window?.rootViewController = RootCoordinator.presentGroups() 21 | window?.makeKeyAndVisible() 22 | 23 | return true 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /MVVMC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /MVVMC/Group/Coordinator/GroupCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupCoordinator.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol GroupCoordinatorProtocol: class { 12 | func showMembers() 13 | } 14 | 15 | final class GroupCoordinator: GroupCoordinatorProtocol { 16 | 17 | private weak var navigationController: UINavigationController? 18 | 19 | init(navigationController: UINavigationController?) { 20 | self.navigationController = navigationController 21 | } 22 | 23 | func showMembers() { 24 | // Show members in a new screen 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVMC/Group/Interactor/GroupInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupInteractor.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol GroupInteractorProtocol: class { 12 | init(group: Group) 13 | 14 | var group: Group { get } 15 | var groupMembersDidChange: ObserverSet { get } 16 | 17 | func add(user: User) 18 | func remove(user: User) 19 | } 20 | 21 | final class GroupInteractor: GroupInteractorProtocol { 22 | 23 | private(set) var group: Group 24 | 25 | var groupMembersDidChange = ObserverSet() 26 | 27 | init(group: Group) { 28 | self.group = group 29 | } 30 | 31 | func add(user: User) { 32 | // Add user to the group 33 | var newMembers = group.members 34 | newMembers.append(user) 35 | group = Group(id: group.id, name: group.name, members: newMembers) 36 | 37 | groupMembersDidChange.notify() 38 | } 39 | 40 | func remove(user: User) { 41 | // Remove user from the group 42 | var newMembers = group.members 43 | guard let indexOfUser = newMembers.index(of: user) else { 44 | return 45 | } 46 | newMembers.remove(at: indexOfUser) 47 | group = Group(id: group.id, name: group.name, members: newMembers) 48 | 49 | groupMembersDidChange.notify() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MVVMC/Group/View/GroupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupViewController.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class GroupViewController: UIViewController { 12 | 13 | private let nameLabel = UILabel(frame: .zero) 14 | 15 | var viewModel: GroupViewModel { 16 | didSet { 17 | self.updateUI() 18 | } 19 | } 20 | 21 | init(viewModel: GroupViewModel) { 22 | self.viewModel = viewModel 23 | super.init(nibName: nil, bundle: nil) 24 | } 25 | 26 | required init?(coder aDecoder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | // MARK: - UIViewController 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | setupUI() 36 | layoutUI() 37 | styleUI() 38 | updateUI() 39 | } 40 | 41 | private func setupUI() { 42 | view.addSubview(nameLabel) 43 | } 44 | 45 | private func layoutUI() { 46 | nameLabel.frame = view.frame 47 | } 48 | 49 | private func styleUI() { 50 | view.backgroundColor = .white 51 | nameLabel.textAlignment = .center 52 | } 53 | 54 | private func updateUI() { 55 | title = viewModel.groupName 56 | nameLabel.text = viewModel.groupName 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MVVMC/Group/ViewModel/GroupViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupViewModel.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class GroupViewModel { 12 | 13 | private let interactor: GroupInteractorProtocol 14 | private let coordinator: GroupCoordinatorProtocol 15 | 16 | private var group: Group { 17 | return interactor.group 18 | } 19 | 20 | init(interactor: GroupInteractorProtocol, coordinator: GroupCoordinatorProtocol) { 21 | self.interactor = interactor 22 | self.coordinator = coordinator 23 | } 24 | 25 | var groupName: String { 26 | return group.name 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MVVMC/Groups/Coordinator/GroupsCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupsCoordinator.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol GroupsCoordinatorProtocol: class { 12 | func present(group: Group) 13 | func dismissGroup() 14 | } 15 | 16 | final class GroupsCoordinator: GroupsCoordinatorProtocol { 17 | 18 | weak var navigationController: UINavigationController? 19 | 20 | func present(group: Group) { 21 | 22 | // Preparing the new calçot 23 | let groupCoordinator = GroupCoordinator(navigationController: navigationController) 24 | let groupInteractor = GroupInteractor(group: group) 25 | let groupViewModel = GroupViewModel(interactor: groupInteractor, coordinator: groupCoordinator) 26 | let groupViewController = GroupViewController(viewModel: groupViewModel) 27 | 28 | // Adding observer listening to group members change 29 | groupInteractor.groupMembersDidChange.add { [weak groupViewController, weak groupInteractor] in 30 | guard let interactor = groupInteractor else { return } 31 | groupViewController?.viewModel = GroupViewModel(interactor: interactor, coordinator: groupCoordinator) 32 | } 33 | 34 | // Navigate to the new screen 35 | navigationController?.pushViewController(groupViewController, animated: true) 36 | } 37 | 38 | func dismissGroup() { 39 | navigationController?.popViewController(animated: true) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVMC/Groups/Coordinator/RootCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootCoordinator.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class RootCoordinator { 12 | 13 | static func presentGroups(groupsList: GroupsList = GroupsList(groups: [], groupsCategory: .joined)) -> UINavigationController { 14 | 15 | // Preparing the new calçot 16 | let groupsCoordinator = GroupsCoordinator() 17 | let groupsInteractor = GroupsInteractor(groupsList: groupsList, dataProvider: GroupDataProvider.shared) 18 | let groupsViewModel = GroupsViewModel(interactor: groupsInteractor, coordinator: groupsCoordinator) 19 | let groupsViewController = GroupsViewController(viewModel: groupsViewModel) 20 | 21 | // Adding observer listening to model change 22 | groupsInteractor.groupsListDidChange.add { [weak groupsViewController, weak groupsInteractor] in 23 | guard let interactor = groupsInteractor else { 24 | return 25 | } 26 | groupsViewController?.viewModel = GroupsViewModel(interactor: interactor, coordinator: groupsCoordinator) 27 | } 28 | 29 | let navigationController = UINavigationController(rootViewController: groupsViewController) 30 | groupsCoordinator.navigationController = navigationController 31 | 32 | return navigationController 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVMC/Groups/Interactor/GroupsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupsInteractor.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol GroupsInteractorProtocol { 12 | var groupsList: GroupsList { get } 13 | var groupsListDidChange: ObserverSet { get } 14 | 15 | func fetchGroups() 16 | } 17 | 18 | final class GroupsInteractor: GroupsInteractorProtocol { 19 | 20 | private(set) var groupsList: GroupsList { 21 | didSet { 22 | groupsListDidChange.notify() 23 | } 24 | } 25 | 26 | var groupsListDidChange = ObserverSet() 27 | 28 | private let dataProvider: GroupDataProvider 29 | 30 | init(groupsList: GroupsList, dataProvider: GroupDataProvider) { 31 | self.groupsList = groupsList 32 | self.dataProvider = dataProvider 33 | } 34 | 35 | func fetchGroups() { 36 | // fetch data from a data provider 37 | dataProvider.fetchGroups() { (groups, error) in 38 | // Error handling? Naaah, this stuff always works :D 39 | 40 | // Update the model with new data 41 | self.groupsList = GroupsList(groups: groups, groupsCategory: .joined) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MVVMC/Groups/Model/GroupsList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupsList.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GroupsList: Equatable { 12 | let groups: [Group] 13 | let groupsCategory: GroupCategory 14 | 15 | static func == (lhs: GroupsList, rhs: GroupsList) -> Bool { 16 | return lhs.groups == rhs.groups && lhs.groupsCategory == rhs.groupsCategory 17 | } 18 | 19 | enum GroupCategory { 20 | case joined 21 | case invited 22 | case suggested 23 | 24 | var localizedString: String { 25 | switch self { 26 | case .invited: 27 | return NSLocalizedString("Invitations", comment: "") 28 | case .joined: 29 | return NSLocalizedString("My groups", comment: "") 30 | case .suggested: 31 | return NSLocalizedString("Suggested groups", comment: "") 32 | } 33 | } 34 | } 35 | } 36 | 37 | struct Group: Equatable { 38 | let id: String 39 | let name: String 40 | let members: [User] 41 | 42 | static func == (lhs: Group, rhs: Group) -> Bool { 43 | return lhs.id == rhs.id && lhs.name == rhs.name && lhs.members == rhs.members 44 | } 45 | } 46 | 47 | struct User: Equatable { 48 | let id: String 49 | let firstName: String 50 | let lastName: String 51 | 52 | static func == (lhs: User, rhs: User) -> Bool { 53 | return lhs.id == rhs.id && lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /MVVMC/Groups/View/GroupTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupTableViewCell.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 04/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GroupTableViewCell: UITableViewCell { 12 | 13 | static let cellIdentifier = "GroupCellIdentifier" 14 | 15 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 16 | super.init(style: .default, reuseIdentifier: reuseIdentifier) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | func configure(viewModel: GroupCellViewModel) { 24 | textLabel?.text = viewModel.groupName 25 | accessoryType = .disclosureIndicator 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MVVMC/Groups/View/GroupsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupsViewController.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class GroupsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 12 | 13 | var viewModel: GroupsViewModel { 14 | didSet { 15 | updateUI() 16 | } 17 | } 18 | 19 | private let tableView = UITableView(frame: .zero) 20 | 21 | init(viewModel: GroupsViewModel) { 22 | self.viewModel = viewModel 23 | super.init(nibName: nil, bundle: nil) 24 | } 25 | 26 | required init?(coder aDecoder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | private func setupUI() { 31 | tableView.delegate = self 32 | tableView.dataSource = self 33 | tableView.register(GroupTableViewCell.self, forCellReuseIdentifier: GroupTableViewCell.cellIdentifier) 34 | 35 | view.addSubview(tableView) 36 | 37 | title = viewModel.groupsCategory 38 | } 39 | 40 | private func layoutUI() { 41 | tableView.frame = view.frame 42 | } 43 | 44 | private func updateUI() { 45 | tableView.reloadData() 46 | } 47 | 48 | // MARK: - UIViewController 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | setupUI() 54 | layoutUI() 55 | viewModel.fetchGroups() 56 | } 57 | 58 | // MARK: - UITableViewDelegate 59 | 60 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 61 | guard let cell = tableView.dequeueReusableCell(withIdentifier: GroupTableViewCell.cellIdentifier, for: indexPath) as? GroupTableViewCell else { 62 | fatalError("Could not dequeue cell with identifier: \(GroupTableViewCell.cellIdentifier)") 63 | } 64 | 65 | let groupCellViewModel = viewModel.data(forRowAt: indexPath.row) 66 | cell.configure(viewModel: groupCellViewModel) 67 | 68 | return cell 69 | } 70 | 71 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 72 | tableView.deselectRow(at: indexPath, animated: true) 73 | viewModel.showGroup(at: indexPath.row) 74 | } 75 | 76 | // MARK: - UITableViewDataSource 77 | 78 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 79 | return viewModel.numberOfRows() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MVVMC/Groups/ViewModel/GroupCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupCellViewModel.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 04/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GroupCellViewModel { 12 | let groupName: String 13 | } 14 | -------------------------------------------------------------------------------- /MVVMC/Groups/ViewModel/GroupsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupsViewModel.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 28/07/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class GroupsViewModel { 12 | 13 | private let interactor: GroupsInteractorProtocol 14 | private let coordinator: GroupsCoordinatorProtocol 15 | 16 | private var groupsList: GroupsList { 17 | return interactor.groupsList 18 | } 19 | 20 | init(interactor: GroupsInteractorProtocol, coordinator: GroupsCoordinatorProtocol) { 21 | self.interactor = interactor 22 | self.coordinator = coordinator 23 | } 24 | 25 | var groupsCategory: String { 26 | return groupsList.groupsCategory.localizedString 27 | } 28 | 29 | func fetchGroups() { 30 | // interaction to be handled within calçot 31 | interactor.fetchGroups() 32 | } 33 | 34 | func dismissGroup() { 35 | // interaction leading to a different screen or calçot 36 | coordinator.dismissGroup() 37 | } 38 | 39 | func data(forRowAt index: Int) -> GroupCellViewModel { 40 | let group = groupsList.groups[index] 41 | return GroupCellViewModel(groupName: group.name) 42 | } 43 | 44 | func showGroup(at index: Int) { 45 | coordinator.present(group: groupsList.groups[index]) 46 | } 47 | 48 | func numberOfRows() -> Int { 49 | return groupsList.groups.count 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MVVMC/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | armv7 26 | 27 | UISupportedInterfaceOrientations 28 | 29 | UIInterfaceOrientationPortrait 30 | UIInterfaceOrientationLandscapeLeft 31 | UIInterfaceOrientationLandscapeRight 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVVMC/Modules/GroupDataProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupDataProvider.swift 3 | // MVVMC 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class GroupDataProvider { 12 | 13 | // Provides data e.g. from database or network 14 | static let shared = GroupDataProvider() 15 | 16 | func fetchGroups(completion: (_ groups: [Group], _ error: Error?) -> Void) { 17 | let groups = (1...50).map { id in Group(id: "\(id)", name: "My group \(id)", members: []) } 18 | completion(groups, nil) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MVVMC/ObserverSet/ObserverSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObserverSet.swift 3 | // ObserverSet 4 | // 5 | // Created by Mike Ash on 1/22/15. 6 | // Copyright (c) 2015 Mike Ash. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class ObserverSetEntry { 12 | fileprivate weak var object: AnyObject? 13 | fileprivate let f: (AnyObject) -> (Parameters) -> Void 14 | 15 | fileprivate init(object: AnyObject, f: @escaping (AnyObject) -> (Parameters) -> Void) { 16 | self.object = object 17 | self.f = f 18 | } 19 | } 20 | 21 | public class ObserverSet: CustomStringConvertible { 22 | // Locking support 23 | 24 | private var queue = DispatchQueue(label: "com.mikeash.ObserverSet", attributes: []) 25 | 26 | private func synchronized(_ f: (Void) -> Void) { 27 | queue.sync(execute: f) 28 | } 29 | 30 | // Main implementation 31 | 32 | private var entries: [ObserverSetEntry] = [] 33 | 34 | public init() {} 35 | 36 | @discardableResult 37 | public func add(_ object: T, _ f: @escaping (T) -> (Parameters) -> Void) -> ObserverSetEntry { 38 | let entry = ObserverSetEntry(object: object, f: { f($0 as! T) }) 39 | synchronized { 40 | self.entries.append(entry) 41 | } 42 | return entry 43 | } 44 | 45 | @discardableResult 46 | public func add(_ f: @escaping (Parameters) -> Void) -> ObserverSetEntry { 47 | return self.add(self, { ignored in f }) 48 | } 49 | 50 | public func remove(_ entry: ObserverSetEntry) { 51 | synchronized { 52 | self.entries = self.entries.filter { $0 !== entry } 53 | } 54 | } 55 | 56 | public func remove(_ object: AnyObject) { 57 | synchronized { 58 | self.entries = self.entries.filter { $0.object !== object } 59 | } 60 | } 61 | 62 | public func notify(_ parameters: Parameters) { 63 | var toCall: [(Parameters) -> Void] = [] 64 | 65 | synchronized { 66 | for entry in self.entries { 67 | if let object: AnyObject = entry.object { 68 | toCall.append(entry.f(object)) 69 | } 70 | } 71 | self.entries = self.entries.filter { $0.object != nil } 72 | } 73 | 74 | for f in toCall { 75 | f(parameters) 76 | } 77 | } 78 | 79 | // Printable 80 | 81 | public var description: String { 82 | var entries: [ObserverSetEntry] = [] 83 | synchronized { 84 | entries = self.entries 85 | } 86 | 87 | let strings = entries.map { 88 | entry in 89 | (entry.object === self 90 | ? "\(entry.f)" 91 | : "\(entry.object) \(entry.f)") 92 | } 93 | let joined = strings.joined(separator: ", ") 94 | 95 | return "\(Mirror(reflecting: self).description): (\(joined))" 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /MVVMC/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runtastic/mvvmc-example/29125efa2a2151ad5a2de3d6841196d55d0d8f06/MVVMC/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # MVVMC - Adapting the MVVM Design Pattern at Runtastic 2 | 3 | Example iOS app describing the MVVMC design pattern at Runtastic. 4 | 5 | The MVVMC is fully described in the following article: [MVVMC - Adapting the MVVM Design Pattern at Runtastic](https://www.runtastic.com/blog/en/tech/mvvmc-adapting-the-mvvm-design-pattern-at-runtastic/) 6 | -------------------------------------------------------------------------------- /UnitTests/GroupsViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnitTests.swift 3 | // UnitTests 4 | // 5 | // Created by Adam Studenic on 03/08/2017. 6 | // Copyright © 2017 runtastic. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | final class GroupsViewModelTests: XCTestCase { 12 | 13 | private var interactor: GroupsFakeInteractor! 14 | private var coordinator: GroupsFakeCoordinator! 15 | private var viewModel: GroupsViewModel! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | 20 | interactor = GroupsFakeInteractor() 21 | coordinator = GroupsFakeCoordinator() 22 | viewModel = GroupsViewModel(interactor: interactor, coordinator: coordinator) 23 | } 24 | 25 | override func tearDown() { 26 | interactor = nil 27 | coordinator = nil 28 | viewModel = nil 29 | super.tearDown() 30 | } 31 | 32 | func testFetchUserGroups() { 33 | let exp = expectation(description: "Fetch groups") 34 | 35 | // This completion block should be called when groups are fetched 36 | interactor.groupsListDidChange.add { [weak interactor] in 37 | XCTAssertEqual(interactor?.groupsList.groups.count, 50) 38 | exp.fulfill() 39 | } 40 | 41 | // Fetch mocked user groups 42 | viewModel.fetchGroups() 43 | 44 | waitForExpectations(timeout: 2.0, handler: nil) 45 | } 46 | } 47 | 48 | // MARK: Mocked interactor 49 | 50 | class GroupsFakeInteractor: GroupsInteractorProtocol { 51 | var groupsListDidChange = ObserverSet() 52 | 53 | var groupsList = GroupsList(groups: [], groupsCategory: .joined) 54 | 55 | func fetchGroups() { 56 | groupsList = GroupsList(groups: (0..<50).map { _ in Group(id: "0", name: "Test", members: []) }, groupsCategory: .joined) 57 | groupsListDidChange.notify() 58 | } 59 | } 60 | 61 | // MARK: Mocked coordinator 62 | 63 | class GroupsFakeCoordinator: GroupsCoordinatorProtocol { 64 | var presented = false 65 | var dismissed = false 66 | 67 | func present(group: Group) { 68 | presented = true 69 | } 70 | 71 | func dismissGroup() { 72 | dismissed = true 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /UnitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | --------------------------------------------------------------------------------