├── MVVM.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── MVVM.xccheckout │ └── xcuserdata │ │ └── carlos.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── carlos.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── MVVM.xcscheme │ └── xcschememanagement.plist ├── MVVM ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── Context.swift ├── DetailViewController.swift ├── DetailViewModel.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ListViewModel.swift ├── MVVM.xcdatamodeld │ ├── .xccurrentversion │ └── MVVM.xcdatamodel │ │ └── contents ├── MasterTableViewController.swift └── Payback.swift ├── MVVMTests ├── DetailViewModelTests.swift ├── Info.plist └── ListViewModelTests.swift └── README.md /MVVM.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 07A753731AD5A2E700284AC3 /* DetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA591AD52570006D228C /* DetailViewModel.swift */; }; 11 | 07A753741AD5A47300284AC3 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5E1AD52607006D228C /* Context.swift */; }; 12 | 07A753751AD5A47600284AC3 /* Payback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5F1AD52607006D228C /* Payback.swift */; }; 13 | 07A753771AD5A4C500284AC3 /* ListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A753761AD5A4C500284AC3 /* ListViewModelTests.swift */; }; 14 | 07A753781AD5A4EC00284AC3 /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5A1AD52570006D228C /* ListViewModel.swift */; }; 15 | 07ACCA331AD523EE006D228C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA321AD523EE006D228C /* AppDelegate.swift */; }; 16 | 07ACCA3B1AD523EE006D228C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07ACCA391AD523EE006D228C /* Main.storyboard */; }; 17 | 07ACCA3D1AD523EE006D228C /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07ACCA3C1AD523EE006D228C /* Images.xcassets */; }; 18 | 07ACCA401AD523EE006D228C /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07ACCA3E1AD523EE006D228C /* LaunchScreen.xib */; }; 19 | 07ACCA4C1AD523EF006D228C /* DetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA4B1AD523EF006D228C /* DetailViewModelTests.swift */; }; 20 | 07ACCA571AD52489006D228C /* MasterTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA561AD52489006D228C /* MasterTableViewController.swift */; }; 21 | 07ACCA5B1AD52570006D228C /* DetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA591AD52570006D228C /* DetailViewModel.swift */; }; 22 | 07ACCA5C1AD52570006D228C /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5A1AD52570006D228C /* ListViewModel.swift */; }; 23 | 07ACCA601AD52607006D228C /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5E1AD52607006D228C /* Context.swift */; }; 24 | 07ACCA611AD52607006D228C /* Payback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA5F1AD52607006D228C /* Payback.swift */; }; 25 | 07ACCA631AD527B0006D228C /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACCA621AD527B0006D228C /* DetailViewController.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 07ACCA461AD523EF006D228C /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 07ACCA251AD523EE006D228C /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 07ACCA2C1AD523EE006D228C; 34 | remoteInfo = MVVM; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 07A753761AD5A4C500284AC3 /* ListViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewModelTests.swift; sourceTree = ""; }; 40 | 07ACCA2D1AD523EE006D228C /* MVVM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MVVM.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 07ACCA311AD523EE006D228C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 07ACCA321AD523EE006D228C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 07ACCA3A1AD523EE006D228C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | 07ACCA3C1AD523EE006D228C /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 45 | 07ACCA3F1AD523EE006D228C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 46 | 07ACCA451AD523EF006D228C /* MVVMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MVVMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 07ACCA4A1AD523EF006D228C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 07ACCA4B1AD523EF006D228C /* DetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewModelTests.swift; sourceTree = ""; }; 49 | 07ACCA561AD52489006D228C /* MasterTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTableViewController.swift; sourceTree = ""; }; 50 | 07ACCA591AD52570006D228C /* DetailViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewModel.swift; sourceTree = ""; }; 51 | 07ACCA5A1AD52570006D228C /* ListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; 52 | 07ACCA5E1AD52607006D228C /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; 53 | 07ACCA5F1AD52607006D228C /* Payback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Payback.swift; sourceTree = ""; }; 54 | 07ACCA621AD527B0006D228C /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 07ACCA2A1AD523EE006D228C /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | 07ACCA421AD523EF006D228C /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXFrameworksBuildPhase section */ 73 | 74 | /* Begin PBXGroup section */ 75 | 07ACCA241AD523EE006D228C = { 76 | isa = PBXGroup; 77 | children = ( 78 | 07ACCA2F1AD523EE006D228C /* MVVM */, 79 | 07ACCA481AD523EF006D228C /* MVVMTests */, 80 | 07ACCA2E1AD523EE006D228C /* Products */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | 07ACCA2E1AD523EE006D228C /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 07ACCA2D1AD523EE006D228C /* MVVM.app */, 88 | 07ACCA451AD523EF006D228C /* MVVMTests.xctest */, 89 | ); 90 | name = Products; 91 | sourceTree = ""; 92 | }; 93 | 07ACCA2F1AD523EE006D228C /* MVVM */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 07ACCA321AD523EE006D228C /* AppDelegate.swift */, 97 | 07ACCA551AD52420006D228C /* Controllers */, 98 | 07ACCA581AD52555006D228C /* View Models */, 99 | 07ACCA5D1AD525F3006D228C /* Models */, 100 | 07ACCA391AD523EE006D228C /* Main.storyboard */, 101 | 07ACCA3C1AD523EE006D228C /* Images.xcassets */, 102 | 07ACCA3E1AD523EE006D228C /* LaunchScreen.xib */, 103 | 07ACCA301AD523EE006D228C /* Supporting Files */, 104 | ); 105 | path = MVVM; 106 | sourceTree = ""; 107 | }; 108 | 07ACCA301AD523EE006D228C /* Supporting Files */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 07ACCA311AD523EE006D228C /* Info.plist */, 112 | ); 113 | name = "Supporting Files"; 114 | sourceTree = ""; 115 | }; 116 | 07ACCA481AD523EF006D228C /* MVVMTests */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 07A753761AD5A4C500284AC3 /* ListViewModelTests.swift */, 120 | 07ACCA4B1AD523EF006D228C /* DetailViewModelTests.swift */, 121 | 07ACCA491AD523EF006D228C /* Supporting Files */, 122 | ); 123 | path = MVVMTests; 124 | sourceTree = ""; 125 | }; 126 | 07ACCA491AD523EF006D228C /* Supporting Files */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 07ACCA4A1AD523EF006D228C /* Info.plist */, 130 | ); 131 | name = "Supporting Files"; 132 | sourceTree = ""; 133 | }; 134 | 07ACCA551AD52420006D228C /* Controllers */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 07ACCA561AD52489006D228C /* MasterTableViewController.swift */, 138 | 07ACCA621AD527B0006D228C /* DetailViewController.swift */, 139 | ); 140 | name = Controllers; 141 | sourceTree = ""; 142 | }; 143 | 07ACCA581AD52555006D228C /* View Models */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 07ACCA5A1AD52570006D228C /* ListViewModel.swift */, 147 | 07ACCA591AD52570006D228C /* DetailViewModel.swift */, 148 | ); 149 | name = "View Models"; 150 | sourceTree = ""; 151 | }; 152 | 07ACCA5D1AD525F3006D228C /* Models */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 07ACCA5E1AD52607006D228C /* Context.swift */, 156 | 07ACCA5F1AD52607006D228C /* Payback.swift */, 157 | ); 158 | name = Models; 159 | sourceTree = ""; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | 07ACCA2C1AD523EE006D228C /* MVVM */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = 07ACCA4F1AD523EF006D228C /* Build configuration list for PBXNativeTarget "MVVM" */; 167 | buildPhases = ( 168 | 07ACCA291AD523EE006D228C /* Sources */, 169 | 07ACCA2A1AD523EE006D228C /* Frameworks */, 170 | 07ACCA2B1AD523EE006D228C /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = MVVM; 177 | productName = MVVM; 178 | productReference = 07ACCA2D1AD523EE006D228C /* MVVM.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | 07ACCA441AD523EF006D228C /* MVVMTests */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = 07ACCA521AD523EF006D228C /* Build configuration list for PBXNativeTarget "MVVMTests" */; 184 | buildPhases = ( 185 | 07ACCA411AD523EF006D228C /* Sources */, 186 | 07ACCA421AD523EF006D228C /* Frameworks */, 187 | 07ACCA431AD523EF006D228C /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | 07ACCA471AD523EF006D228C /* PBXTargetDependency */, 193 | ); 194 | name = MVVMTests; 195 | productName = MVVMTests; 196 | productReference = 07ACCA451AD523EF006D228C /* MVVMTests.xctest */; 197 | productType = "com.apple.product-type.bundle.unit-test"; 198 | }; 199 | /* End PBXNativeTarget section */ 200 | 201 | /* Begin PBXProject section */ 202 | 07ACCA251AD523EE006D228C /* Project object */ = { 203 | isa = PBXProject; 204 | attributes = { 205 | LastSwiftMigration = 0710; 206 | LastSwiftUpdateCheck = 0710; 207 | LastUpgradeCheck = 0630; 208 | ORGANIZATIONNAME = "Carlos García"; 209 | TargetAttributes = { 210 | 07ACCA2C1AD523EE006D228C = { 211 | CreatedOnToolsVersion = 6.3; 212 | }; 213 | 07ACCA441AD523EF006D228C = { 214 | CreatedOnToolsVersion = 6.3; 215 | TestTargetID = 07ACCA2C1AD523EE006D228C; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 07ACCA281AD523EE006D228C /* Build configuration list for PBXProject "MVVM" */; 220 | compatibilityVersion = "Xcode 3.2"; 221 | developmentRegion = English; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | en, 225 | Base, 226 | ); 227 | mainGroup = 07ACCA241AD523EE006D228C; 228 | productRefGroup = 07ACCA2E1AD523EE006D228C /* Products */; 229 | projectDirPath = ""; 230 | projectRoot = ""; 231 | targets = ( 232 | 07ACCA2C1AD523EE006D228C /* MVVM */, 233 | 07ACCA441AD523EF006D228C /* MVVMTests */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | 07ACCA2B1AD523EE006D228C /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | 07ACCA3B1AD523EE006D228C /* Main.storyboard in Resources */, 244 | 07ACCA401AD523EE006D228C /* LaunchScreen.xib in Resources */, 245 | 07ACCA3D1AD523EE006D228C /* Images.xcassets in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | 07ACCA431AD523EF006D228C /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 07ACCA291AD523EE006D228C /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 07ACCA611AD52607006D228C /* Payback.swift in Sources */, 264 | 07ACCA5C1AD52570006D228C /* ListViewModel.swift in Sources */, 265 | 07ACCA631AD527B0006D228C /* DetailViewController.swift in Sources */, 266 | 07ACCA331AD523EE006D228C /* AppDelegate.swift in Sources */, 267 | 07ACCA571AD52489006D228C /* MasterTableViewController.swift in Sources */, 268 | 07ACCA601AD52607006D228C /* Context.swift in Sources */, 269 | 07ACCA5B1AD52570006D228C /* DetailViewModel.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 07ACCA411AD523EF006D228C /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 07A753741AD5A47300284AC3 /* Context.swift in Sources */, 278 | 07ACCA4C1AD523EF006D228C /* DetailViewModelTests.swift in Sources */, 279 | 07A753781AD5A4EC00284AC3 /* ListViewModel.swift in Sources */, 280 | 07A753731AD5A2E700284AC3 /* DetailViewModel.swift in Sources */, 281 | 07A753771AD5A4C500284AC3 /* ListViewModelTests.swift in Sources */, 282 | 07A753751AD5A47600284AC3 /* Payback.swift in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | /* End PBXSourcesBuildPhase section */ 287 | 288 | /* Begin PBXTargetDependency section */ 289 | 07ACCA471AD523EF006D228C /* PBXTargetDependency */ = { 290 | isa = PBXTargetDependency; 291 | target = 07ACCA2C1AD523EE006D228C /* MVVM */; 292 | targetProxy = 07ACCA461AD523EF006D228C /* PBXContainerItemProxy */; 293 | }; 294 | /* End PBXTargetDependency section */ 295 | 296 | /* Begin PBXVariantGroup section */ 297 | 07ACCA391AD523EE006D228C /* Main.storyboard */ = { 298 | isa = PBXVariantGroup; 299 | children = ( 300 | 07ACCA3A1AD523EE006D228C /* Base */, 301 | ); 302 | name = Main.storyboard; 303 | sourceTree = ""; 304 | }; 305 | 07ACCA3E1AD523EE006D228C /* LaunchScreen.xib */ = { 306 | isa = PBXVariantGroup; 307 | children = ( 308 | 07ACCA3F1AD523EE006D228C /* Base */, 309 | ); 310 | name = LaunchScreen.xib; 311 | sourceTree = ""; 312 | }; 313 | /* End PBXVariantGroup section */ 314 | 315 | /* Begin XCBuildConfiguration section */ 316 | 07ACCA4D1AD523EF006D228C /* Debug */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | ALWAYS_SEARCH_USER_PATHS = NO; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BOOL_CONVERSION = YES; 325 | CLANG_WARN_CONSTANT_CONVERSION = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_EMPTY_BODY = YES; 328 | CLANG_WARN_ENUM_CONVERSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu99; 338 | GCC_DYNAMIC_NO_PIC = NO; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_OPTIMIZATION_LEVEL = 0; 341 | GCC_PREPROCESSOR_DEFINITIONS = ( 342 | "DEBUG=1", 343 | "$(inherited)", 344 | ); 345 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 350 | GCC_WARN_UNUSED_FUNCTION = YES; 351 | GCC_WARN_UNUSED_VARIABLE = YES; 352 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 353 | MTL_ENABLE_DEBUG_INFO = YES; 354 | ONLY_ACTIVE_ARCH = YES; 355 | SDKROOT = iphoneos; 356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 357 | }; 358 | name = Debug; 359 | }; 360 | 07ACCA4E1AD523EF006D228C /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 365 | CLANG_CXX_LIBRARY = "libc++"; 366 | CLANG_ENABLE_MODULES = YES; 367 | CLANG_ENABLE_OBJC_ARC = YES; 368 | CLANG_WARN_BOOL_CONVERSION = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 371 | CLANG_WARN_EMPTY_BODY = YES; 372 | CLANG_WARN_ENUM_CONVERSION = YES; 373 | CLANG_WARN_INT_CONVERSION = YES; 374 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 375 | CLANG_WARN_UNREACHABLE_CODE = YES; 376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 377 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 378 | COPY_PHASE_STRIP = NO; 379 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 380 | ENABLE_NS_ASSERTIONS = NO; 381 | ENABLE_STRICT_OBJC_MSGSEND = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu99; 383 | GCC_NO_COMMON_BLOCKS = YES; 384 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 385 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 386 | GCC_WARN_UNDECLARED_SELECTOR = YES; 387 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 388 | GCC_WARN_UNUSED_FUNCTION = YES; 389 | GCC_WARN_UNUSED_VARIABLE = YES; 390 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 391 | MTL_ENABLE_DEBUG_INFO = NO; 392 | SDKROOT = iphoneos; 393 | VALIDATE_PRODUCT = YES; 394 | }; 395 | name = Release; 396 | }; 397 | 07ACCA501AD523EF006D228C /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 401 | INFOPLIST_FILE = MVVM/Info.plist; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 403 | PRODUCT_NAME = "$(TARGET_NAME)"; 404 | }; 405 | name = Debug; 406 | }; 407 | 07ACCA511AD523EF006D228C /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 411 | INFOPLIST_FILE = MVVM/Info.plist; 412 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 413 | PRODUCT_NAME = "$(TARGET_NAME)"; 414 | }; 415 | name = Release; 416 | }; 417 | 07ACCA531AD523EF006D228C /* Debug */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | BUNDLE_LOADER = "$(TEST_HOST)"; 421 | FRAMEWORK_SEARCH_PATHS = ( 422 | "$(SDKROOT)/Developer/Library/Frameworks", 423 | "$(inherited)", 424 | ); 425 | GCC_PREPROCESSOR_DEFINITIONS = ( 426 | "DEBUG=1", 427 | "$(inherited)", 428 | ); 429 | INFOPLIST_FILE = MVVMTests/Info.plist; 430 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 431 | PRODUCT_NAME = "$(TARGET_NAME)"; 432 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVM.app/MVVM"; 433 | }; 434 | name = Debug; 435 | }; 436 | 07ACCA541AD523EF006D228C /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | BUNDLE_LOADER = "$(TEST_HOST)"; 440 | FRAMEWORK_SEARCH_PATHS = ( 441 | "$(SDKROOT)/Developer/Library/Frameworks", 442 | "$(inherited)", 443 | ); 444 | INFOPLIST_FILE = MVVMTests/Info.plist; 445 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVM.app/MVVM"; 448 | }; 449 | name = Release; 450 | }; 451 | /* End XCBuildConfiguration section */ 452 | 453 | /* Begin XCConfigurationList section */ 454 | 07ACCA281AD523EE006D228C /* Build configuration list for PBXProject "MVVM" */ = { 455 | isa = XCConfigurationList; 456 | buildConfigurations = ( 457 | 07ACCA4D1AD523EF006D228C /* Debug */, 458 | 07ACCA4E1AD523EF006D228C /* Release */, 459 | ); 460 | defaultConfigurationIsVisible = 0; 461 | defaultConfigurationName = Release; 462 | }; 463 | 07ACCA4F1AD523EF006D228C /* Build configuration list for PBXNativeTarget "MVVM" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 07ACCA501AD523EF006D228C /* Debug */, 467 | 07ACCA511AD523EF006D228C /* Release */, 468 | ); 469 | defaultConfigurationIsVisible = 0; 470 | defaultConfigurationName = Release; 471 | }; 472 | 07ACCA521AD523EF006D228C /* Build configuration list for PBXNativeTarget "MVVMTests" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 07ACCA531AD523EF006D228C /* Debug */, 476 | 07ACCA541AD523EF006D228C /* Release */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | /* End XCConfigurationList section */ 482 | }; 483 | rootObject = 07ACCA251AD523EE006D228C /* Project object */; 484 | } 485 | -------------------------------------------------------------------------------- /MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVVM.xcodeproj/project.xcworkspace/xcshareddata/MVVM.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | CC4E0758-A1FB-4132-99C7-0223CB0A2440 9 | IDESourceControlProjectName 10 | MVVM 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 409F780C5BC05064F768718FCFF2385825F6CAFA 14 | https://github.com/carlosypunto/MVVMSwiftSample.git 15 | 16 | IDESourceControlProjectPath 17 | MVVM.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 409F780C5BC05064F768718FCFF2385825F6CAFA 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/carlosypunto/MVVMSwiftSample.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 409F780C5BC05064F768718FCFF2385825F6CAFA 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 409F780C5BC05064F768718FCFF2385825F6CAFA 36 | IDESourceControlWCCName 37 | MVVMSwiftSample 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVVM.xcodeproj/project.xcworkspace/xcuserdata/carlos.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosypunto/MVVMSwiftSample/3b364aaf3ecdce8aa3abc3628cda0b820d839cea/MVVM.xcodeproj/project.xcworkspace/xcuserdata/carlos.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MVVM.xcodeproj/xcuserdata/carlos.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /MVVM.xcodeproj/xcuserdata/carlos.xcuserdatad/xcschemes/MVVM.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /MVVM.xcodeproj/xcuserdata/carlos.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MVVM.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 07ACCA2C1AD523EE006D228C 16 | 17 | primary 18 | 19 | 20 | 07ACCA441AD523EF006D228C 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MVVM/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVM 4 | // 5 | // Created by carlos on 8/4/15. 6 | // Copyright (c) 2015 Carlos García. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /MVVM/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVVM/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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /MVVM/Context.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public class Context { 5 | 6 | static let defaultContext = Context() // Singleton 7 | 8 | var paybacks = [Payback]() 9 | 10 | func addPayback(payback: Payback) { 11 | paybacks.insert(payback, atIndex: 0) 12 | } 13 | 14 | func editPayback(index: Int, firstName: String, lastname: String, amount: Double, updated: NSDate) { 15 | let payback = paybacks[index] 16 | payback.firstName = firstName 17 | payback.lastName = lastname 18 | payback.amount = amount 19 | payback.updatedAt = updated 20 | } 21 | 22 | func removePayback(index: Int) { 23 | paybacks.removeAtIndex(index) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /MVVM/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddViewController.swift 3 | // MVVM 4 | // 5 | // Created by carlos on 8/4/15. 6 | // Copyright (c) 2015 Carlos García. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DetailViewController: UIViewController, DetailViewModelDelegate { 12 | 13 | var viewModel: DetailViewModel! 14 | @IBOutlet weak var nameField: UITextField! 15 | @IBOutlet weak var amountField: UITextField! 16 | @IBOutlet weak var resultLabel: UILabel! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | navigationItem.title = viewModel.title 21 | nameField.text = viewModel.name 22 | amountField.text = viewModel.amount 23 | nameField.becomeFirstResponder() 24 | 25 | nameField.addTarget(self, action: "nameChanged", forControlEvents: UIControlEvents.EditingChanged) 26 | amountField.addTarget(self, action: "ammountChanged", forControlEvents: UIControlEvents.EditingChanged) 27 | } 28 | 29 | func nameChanged() { 30 | viewModel.name = nameField.text! 31 | resultLabel.text = viewModel.infoText 32 | } 33 | 34 | func ammountChanged() { 35 | viewModel.amount = amountField.text! 36 | resultLabel.text = viewModel.infoText 37 | } 38 | 39 | 40 | // MARK: - AddViewModelDelegate 41 | 42 | func showInvalidName() { 43 | UIAlertView(title: "Error", message: "Invalid name", delegate: nil, cancelButtonTitle: "OK").show() 44 | nameField.becomeFirstResponder() 45 | } 46 | 47 | func showInvalidAmount() { 48 | UIAlertView(title: "Error", message: "Invalid amount", delegate: nil, cancelButtonTitle: "OK").show() 49 | amountField.becomeFirstResponder() 50 | } 51 | 52 | func dismissAddView() { 53 | navigationController?.popViewControllerAnimated(true) 54 | } 55 | 56 | 57 | 58 | // MARK: - IBActions 59 | 60 | @IBAction func cancelPressed(sender: AnyObject) { 61 | navigationController?.popViewControllerAnimated(true) 62 | } 63 | 64 | @IBAction func donePressed(sender: AnyObject) { 65 | viewModel.handleDonePressed() 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /MVVM/DetailViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public class DetailViewModel { 5 | 6 | public let context: Context = Context.defaultContext 7 | public var title = "New Payback" 8 | public var name = "" 9 | public var amount = "" 10 | public weak var delegate: DetailViewModelDelegate? 11 | 12 | public var infoText: String { 13 | let names = nameComponents 14 | let amount = (self.amount as NSString).doubleValue 15 | return "\(name)\n\(amount)" 16 | } 17 | 18 | private var index: Int = -1 19 | 20 | var isNew: Bool { 21 | return index == -1 22 | } 23 | 24 | // new initializer 25 | public init(delegate: DetailViewModelDelegate) { 26 | self.delegate = delegate 27 | } 28 | 29 | // edit initializer 30 | public convenience init(delegate: DetailViewModelDelegate, index: Int) { 31 | self.init(delegate: delegate) 32 | self.index = index 33 | print(index) 34 | title = "Edit Payback" 35 | let payback = context.paybacks[index] 36 | name = payback.firstName + " " + payback.lastName 37 | amount = "\(payback.amount)" 38 | } 39 | 40 | public func handleDonePressed() { 41 | if !validateName() { 42 | delegate?.showInvalidName() 43 | } 44 | else if !validateAmount() { 45 | delegate?.showInvalidAmount() 46 | } 47 | else { 48 | if isNew { 49 | addPayback() 50 | } 51 | else { 52 | savePayback() 53 | } 54 | delegate?.dismissAddView() 55 | } 56 | } 57 | 58 | private var nameComponents : [String] { 59 | return name.componentsSeparatedByString(" ").filter { !$0.isEmpty } 60 | } 61 | 62 | 63 | func validateName() -> Bool { 64 | return nameComponents.count >= 2 65 | } 66 | 67 | func validateAmount() -> Bool { 68 | let value = (amount as NSString).doubleValue 69 | return value.isNormal && value > 0 70 | } 71 | 72 | func addPayback() { 73 | let names = nameComponents 74 | let amount = (self.amount as NSString).doubleValue 75 | let payback = Payback(firstName: names[0], lastName: names[1], createdAt: NSDate(), amount: amount) 76 | context.addPayback(payback) 77 | } 78 | 79 | func savePayback() { 80 | let names = nameComponents 81 | let amount = (self.amount as NSString).doubleValue 82 | context.editPayback(index, firstName: names[0], lastname: names[1], amount: amount, updated: NSDate()) 83 | } 84 | 85 | } 86 | 87 | public protocol DetailViewModelDelegate: class { 88 | func dismissAddView() 89 | func showInvalidName() 90 | func showInvalidAmount() 91 | } 92 | -------------------------------------------------------------------------------- /MVVM/Images.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 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /MVVM/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.cgn.$(PRODUCT_NAME:rfc1034identifier) 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 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /MVVM/ListViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public class ListViewModel { 5 | 6 | public let context = Context.defaultContext 7 | public var items = [Item]() 8 | 9 | public func refresh() { 10 | items = context.paybacks.map { self.itemForPayback($0) } 11 | print(items) 12 | } 13 | 14 | func itemForPayback(payback: Payback) -> Item { 15 | let singleLetter = payback.lastName.substringToIndex(payback.lastName.startIndex.successor()) 16 | 17 | let title = "\(payback.firstName) \(singleLetter)." 18 | let subtitle = NSDateFormatter.localizedStringFromDate(payback.createdAt, dateStyle: NSDateFormatterStyle.LongStyle, timeStyle: NSDateFormatterStyle.NoStyle) 19 | 20 | let rounded = NSNumber(double: round(payback.amount)).longLongValue 21 | let amount = "$\(rounded)" 22 | 23 | let item = Item(title: title, subtitle: subtitle, amount: amount) 24 | 25 | return item 26 | } 27 | 28 | func removePayback(index: Int) { 29 | context.removePayback(index) 30 | } 31 | 32 | public struct Item { 33 | public let title: String 34 | public let subtitle: String 35 | public let amount: String 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /MVVM/MVVM.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MVVM/MVVM.xcdatamodeld/MVVM.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MVVM/MasterTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MasterTableViewController.swift 3 | // MVVM 4 | // 5 | // Created by carlos on 8/4/15. 6 | // Copyright (c) 2015 Carlos García. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MasterTableViewController: UITableViewController { 12 | 13 | let viewModel = ListViewModel() 14 | 15 | override func viewDidAppear(animated: Bool) { 16 | super.viewDidAppear(animated) 17 | refresh() 18 | } 19 | 20 | // MARK: - Table view data source 21 | 22 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 23 | return viewModel.items.count 24 | } 25 | 26 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 27 | let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) 28 | 29 | let item = viewModel.items[indexPath.row] 30 | cell.textLabel?.text = item.title 31 | cell.detailTextLabel?.text = item.amount 32 | 33 | return cell 34 | } 35 | 36 | override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { 37 | return true 38 | } 39 | 40 | override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { 41 | if editingStyle == .Delete { 42 | viewModel.removePayback(indexPath.row) 43 | refresh() 44 | } 45 | } 46 | 47 | // MARK: - IBActions 48 | 49 | func refresh() { 50 | viewModel.refresh() 51 | tableView.reloadData() 52 | } 53 | 54 | // MARK: - Navigation 55 | 56 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 57 | let vc = segue.destinationViewController as! DetailViewController 58 | if segue.identifier == "createSegue" { 59 | vc.viewModel = DetailViewModel(delegate: vc) 60 | } 61 | else if segue.identifier == "editSegue" { 62 | vc.viewModel = DetailViewModel(delegate: vc, index: tableView.indexPathForSelectedRow!.row) 63 | } 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /MVVM/Payback.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | class Payback: Equatable { 5 | 6 | var firstName: String 7 | var lastName: String 8 | var createdAt: NSDate 9 | var updatedAt: NSDate 10 | var amount: Double 11 | 12 | init(firstName: String, lastName: String, createdAt: NSDate, amount: Double) { 13 | self.firstName = firstName 14 | self.lastName = lastName 15 | self.createdAt = createdAt 16 | self.updatedAt = createdAt 17 | self.amount = amount 18 | } 19 | 20 | } 21 | 22 | func ==(l: Payback, r: Payback) -> Bool { 23 | return l.firstName == r.firstName && 24 | l.lastName == r.lastName && 25 | l.createdAt == r.createdAt && 26 | l.amount == r.amount 27 | } -------------------------------------------------------------------------------- /MVVMTests/DetailViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVMTests.swift 3 | // MVVMTests 4 | // 5 | // Created by carlos on 8/4/15. 6 | // Copyright (c) 2015 Carlos García. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class DetailViewModelTests: XCTestCase, DetailViewModelDelegate { 13 | 14 | var detailViewModel: DetailViewModel! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | Context.defaultContext.paybacks = [Payback]() 19 | detailViewModel = DetailViewModel(delegate: self) 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | detailViewModel.delegate = nil 25 | resetFlags() 26 | } 27 | 28 | // ---------------------------------------------------------------------------------------- 29 | 30 | 31 | func testNoData() { 32 | detailViewModel.handleDonePressed() 33 | XCTAssert(!dismissCalled && (invalidNameCalled || invalidAmountCalled)) 34 | } 35 | 36 | func testInvalidName() { 37 | detailViewModel.amount = "84.00" 38 | 39 | for name in ["", "Jo", "John", "John ", " John"] { 40 | detailViewModel.name = name 41 | 42 | detailViewModel.handleDonePressed() 43 | XCTAssert(!dismissCalled && invalidNameCalled) 44 | resetFlags() 45 | } 46 | } 47 | 48 | func testInvalidAmount() { 49 | detailViewModel.name = "John Appleseed" 50 | 51 | for amount in ["", "Abc", "A19", "..", "0"] { 52 | detailViewModel.amount = amount 53 | 54 | detailViewModel.handleDonePressed() 55 | XCTAssert(!dismissCalled && invalidAmountCalled) 56 | resetFlags() 57 | } 58 | } 59 | 60 | func testAddPlayback() { 61 | detailViewModel.name = "John Appleseed" 62 | detailViewModel.amount = "84.0" 63 | detailViewModel.handleDonePressed() 64 | 65 | let isEqual = Context.defaultContext.paybacks[0] == Payback(firstName: "John", lastName: "Appleseed", createdAt: Context.defaultContext.paybacks[0].createdAt, amount: 84.0) 66 | XCTAssertTrue(isEqual, "") 67 | } 68 | 69 | func testSavePlayback() { 70 | detailViewModel.name = "John Appleseed" 71 | detailViewModel.amount = "84.0" 72 | detailViewModel.handleDonePressed() 73 | 74 | let detailViewModel2 = DetailViewModel(delegate: self, index: 0) 75 | detailViewModel2.name = "Carlos García" 76 | detailViewModel2.amount = "90.0" 77 | detailViewModel2.handleDonePressed() 78 | 79 | let isEqual = Context.defaultContext.paybacks[0] == Payback(firstName: "Carlos", lastName: "García", createdAt: Context.defaultContext.paybacks[0].createdAt, amount: 90.0) 80 | XCTAssertTrue(isEqual) 81 | } 82 | 83 | // ---------------------------------------------------------------------------------------- 84 | 85 | 86 | 87 | // ---------------------------------------------------------------------------------------- 88 | 89 | var dismissCalled = false 90 | var invalidNameCalled = false 91 | var invalidAmountCalled = false 92 | 93 | func resetFlags() { 94 | dismissCalled = false 95 | invalidNameCalled = false 96 | invalidAmountCalled = false 97 | } 98 | 99 | func dismissAddView() { 100 | dismissCalled = true 101 | } 102 | 103 | func showInvalidName() { 104 | invalidNameCalled = true 105 | } 106 | 107 | func showInvalidAmount() { 108 | invalidAmountCalled = true 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MVVMTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.cgn.$(PRODUCT_NAME:rfc1034identifier) 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 | -------------------------------------------------------------------------------- /MVVMTests/ListViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListViewModelTests.swift 3 | // MVVM 4 | // 5 | // Created by carlos on 8/4/15. 6 | // Copyright (c) 2015 Carlos García. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class ListViewModelTests: XCTestCase, DetailViewModelDelegate { 13 | 14 | var list: ListViewModel! 15 | var detail: DetailViewModel! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | Context.defaultContext.paybacks = [Payback]() 20 | list = ListViewModel() 21 | detail = DetailViewModel(delegate: self) 22 | } 23 | 24 | override func tearDown() { 25 | super.tearDown() 26 | detail.delegate = nil 27 | } 28 | 29 | // ---------------------------------------------------------------------------------------- 30 | 31 | func testEmpty() { 32 | let items = list.items 33 | XCTAssert(items.isEmpty) 34 | } 35 | 36 | func testNotEmpty() { 37 | detail.name = "John Appleseed" 38 | detail.amount = "84" 39 | detail.handleDonePressed() 40 | 41 | list.refresh() 42 | XCTAssertTrue(list.items.count == 1) 43 | XCTAssertTrue(list.items[0].title == "John A.") 44 | } 45 | 46 | func testDelete() { 47 | detail.name = "John Appleseed" 48 | detail.amount = "84" 49 | detail.handleDonePressed() 50 | 51 | list.refresh() 52 | XCTAssertTrue(list.items.count == 1) 53 | list.removePayback(0) 54 | list.refresh() 55 | XCTAssertTrue(list.items.count == 0) 56 | } 57 | 58 | // ---------------------------------------------------------------------------------------- 59 | 60 | func showAddView() { 61 | } 62 | 63 | func dismissAddView() { 64 | } 65 | 66 | func showInvalidAmount() { 67 | } 68 | 69 | func showInvalidName() { 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVMSwiftSample 2 | 3 | ### MVVM (Model - View - View Model) Pattern Sample App in Swift 2.1. 4 | 5 | Based on [SwiftDemoMVVM](https://github.com/shilgapira/SwiftDemoMVVM) width CRUD added and using Storyboard. 6 | --------------------------------------------------------------------------------