From b1c555d0a32bc6e61a19a83d47ab57f313e2021f Mon Sep 17 00:00:00 2001 From: William Muro Date: Mon, 21 Mar 2016 13:53:22 -0500 Subject: [PATCH] Added files via upload --- Culqi-PruebaMovil.xcodeproj/project.pbxproj | 502 ++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 45768 bytes .../xcschemes/Culqi-PruebaMovil.xcscheme | 91 ++ .../xcschemes/xcschememanagement.plist | 22 + Culqi-PruebaMovil/AppDelegate.h | 17 + Culqi-PruebaMovil/AppDelegate.m | 45 + .../AppIcon.appiconset/Contents.json | 73 + .../Base.lproj/LaunchScreen.storyboard | 27 + Culqi-PruebaMovil/Base.lproj/Main.storyboard | 159 ++ .../Classes/BIZPopupViewController.h | 33 + .../Classes/BIZPopupViewController.m | 292 ++++ .../Classes/BIZPopupViewControllerDelegate.h | 16 + .../UIViewController+BIZChildViewController.h | 16 + .../UIViewController+BIZChildViewController.m | 36 + Culqi-PruebaMovil/CrearVenta.h | 18 + Culqi-PruebaMovil/CrearVenta.m | 13 + Culqi-PruebaMovil/Info.plist | 47 + .../Resources/k-popup-dismiss-button@2x.png | Bin 0 -> 6252 bytes .../Resources/k-popup-dismiss-button@3x.png | Bin 0 -> 10360 bytes .../TPKeyboardAvoidingCollectionView.h | 15 + .../TPKeyboardAvoidingCollectionView.m | 112 ++ .../TPKeyboardAvoidingScrollView.h | 16 + .../TPKeyboardAvoidingScrollView.m | 91 ++ .../TPKeyboardAvoidingTableView.h | 15 + .../TPKeyboardAvoidingTableView.m | 116 ++ ...UIScrollView+TPKeyboardAvoidingAdditions.h | 22 + ...UIScrollView+TPKeyboardAvoidingAdditions.m | 363 +++++ Culqi-PruebaMovil/ViewController.h | 24 + Culqi-PruebaMovil/ViewController.m | 311 ++++ Culqi-PruebaMovil/main.m | 16 + JSONModel/Info.plist | 26 + JSONModel/JSONModel/JSONModel.h | 350 +++++ JSONModel/JSONModel/JSONModel.m | 1396 +++++++++++++++++ JSONModel/JSONModel/JSONModelArray.h | 60 + JSONModel/JSONModel/JSONModelArray.m | 145 ++ JSONModel/JSONModel/JSONModelClassProperty.h | 73 + JSONModel/JSONModel/JSONModelClassProperty.m | 60 + JSONModel/JSONModel/JSONModelError.h | 114 ++ JSONModel/JSONModel/JSONModelError.m | 93 ++ .../JSONModelCategories/NSArray+JSONModel.h | 40 + .../JSONModelCategories/NSArray+JSONModel.m | 28 + JSONModel/JSONModelLib.h | 35 + JSONModel/JSONModelNetworking/JSONAPI.h | 90 ++ JSONModel/JSONModelNetworking/JSONAPI.m | 152 ++ .../JSONModelNetworking/JSONHTTPClient.h | 175 +++ .../JSONModelNetworking/JSONHTTPClient.m | 369 +++++ .../JSONModel+networking.h | 66 + .../JSONModel+networking.m | 109 ++ .../JSONModelTransformations/JSONKeyMapper.h | 102 ++ .../JSONModelTransformations/JSONKeyMapper.m | 237 +++ .../JSONValueTransformer.h | 230 +++ .../JSONValueTransformer.m | 274 ++++ 53 files changed, 6739 insertions(+) create mode 100644 Culqi-PruebaMovil.xcodeproj/project.pbxproj create mode 100644 Culqi-PruebaMovil.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Culqi-PruebaMovil.xcodeproj/project.xcworkspace/xcuserdata/William.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/Culqi-PruebaMovil.xcscheme create mode 100644 Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Culqi-PruebaMovil/AppDelegate.h create mode 100644 Culqi-PruebaMovil/AppDelegate.m create mode 100644 Culqi-PruebaMovil/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Culqi-PruebaMovil/Base.lproj/LaunchScreen.storyboard create mode 100644 Culqi-PruebaMovil/Base.lproj/Main.storyboard create mode 100644 Culqi-PruebaMovil/Classes/BIZPopupViewController.h create mode 100644 Culqi-PruebaMovil/Classes/BIZPopupViewController.m create mode 100644 Culqi-PruebaMovil/Classes/BIZPopupViewControllerDelegate.h create mode 100644 Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.h create mode 100644 Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.m create mode 100644 Culqi-PruebaMovil/CrearVenta.h create mode 100644 Culqi-PruebaMovil/CrearVenta.m create mode 100644 Culqi-PruebaMovil/Info.plist create mode 100644 Culqi-PruebaMovil/Resources/k-popup-dismiss-button@2x.png create mode 100644 Culqi-PruebaMovil/Resources/k-popup-dismiss-button@3x.png create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h create mode 100644 Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m create mode 100644 Culqi-PruebaMovil/ViewController.h create mode 100644 Culqi-PruebaMovil/ViewController.m create mode 100644 Culqi-PruebaMovil/main.m create mode 100644 JSONModel/Info.plist create mode 100644 JSONModel/JSONModel/JSONModel.h create mode 100644 JSONModel/JSONModel/JSONModel.m create mode 100644 JSONModel/JSONModel/JSONModelArray.h create mode 100644 JSONModel/JSONModel/JSONModelArray.m create mode 100644 JSONModel/JSONModel/JSONModelClassProperty.h create mode 100644 JSONModel/JSONModel/JSONModelClassProperty.m create mode 100644 JSONModel/JSONModel/JSONModelError.h create mode 100644 JSONModel/JSONModel/JSONModelError.m create mode 100644 JSONModel/JSONModelCategories/NSArray+JSONModel.h create mode 100644 JSONModel/JSONModelCategories/NSArray+JSONModel.m create mode 100644 JSONModel/JSONModelLib.h create mode 100644 JSONModel/JSONModelNetworking/JSONAPI.h create mode 100644 JSONModel/JSONModelNetworking/JSONAPI.m create mode 100644 JSONModel/JSONModelNetworking/JSONHTTPClient.h create mode 100644 JSONModel/JSONModelNetworking/JSONHTTPClient.m create mode 100644 JSONModel/JSONModelNetworking/JSONModel+networking.h create mode 100644 JSONModel/JSONModelNetworking/JSONModel+networking.m create mode 100644 JSONModel/JSONModelTransformations/JSONKeyMapper.h create mode 100644 JSONModel/JSONModelTransformations/JSONKeyMapper.m create mode 100644 JSONModel/JSONModelTransformations/JSONValueTransformer.h create mode 100644 JSONModel/JSONModelTransformations/JSONValueTransformer.m diff --git a/Culqi-PruebaMovil.xcodeproj/project.pbxproj b/Culqi-PruebaMovil.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a348e4e --- /dev/null +++ b/Culqi-PruebaMovil.xcodeproj/project.pbxproj @@ -0,0 +1,502 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3D1157FE1C456919009A232B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D1157FD1C456919009A232B /* main.m */; }; + 3D1158011C456919009A232B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D1158001C456919009A232B /* AppDelegate.m */; }; + 3D1158041C456919009A232B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D1158031C456919009A232B /* ViewController.m */; }; + 3D1158071C456919009A232B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D1158051C456919009A232B /* Main.storyboard */; }; + 3D1158091C456919009A232B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D1158081C456919009A232B /* Assets.xcassets */; }; + 3D11580C1C456919009A232B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D11580A1C456919009A232B /* LaunchScreen.storyboard */; }; + 3D1158141C45C073009A232B /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D1158131C45C073009A232B /* WebKit.framework */; }; + 3D928F0E1C48021800611982 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3D928EF41C48021800611982 /* Info.plist */; }; + 3D928F0F1C48021800611982 /* JSONModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928EF71C48021800611982 /* JSONModel.m */; }; + 3D928F101C48021800611982 /* JSONModelArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928EF91C48021800611982 /* JSONModelArray.m */; }; + 3D928F111C48021800611982 /* JSONModelClassProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928EFB1C48021800611982 /* JSONModelClassProperty.m */; }; + 3D928F121C48021800611982 /* JSONModelError.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928EFD1C48021800611982 /* JSONModelError.m */; }; + 3D928F131C48021800611982 /* NSArray+JSONModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F001C48021800611982 /* NSArray+JSONModel.m */; }; + 3D928F141C48021800611982 /* JSONAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F041C48021800611982 /* JSONAPI.m */; }; + 3D928F151C48021800611982 /* JSONHTTPClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F061C48021800611982 /* JSONHTTPClient.m */; }; + 3D928F161C48021800611982 /* JSONModel+networking.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F081C48021800611982 /* JSONModel+networking.m */; }; + 3D928F171C48021800611982 /* JSONKeyMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F0B1C48021800611982 /* JSONKeyMapper.m */; }; + 3D928F181C48021800611982 /* JSONValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F0D1C48021800611982 /* JSONValueTransformer.m */; }; + 3D928F1A1C48022700611982 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D928F191C48022700611982 /* SystemConfiguration.framework */; }; + 3D928F1E1C4808AF00611982 /* CrearVenta.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D928F1D1C4808AF00611982 /* CrearVenta.m */; }; + 3DE37F9A1C9C523E00C28031 /* k-popup-dismiss-button@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DE37F921C9C523E00C28031 /* k-popup-dismiss-button@2x.png */; }; + 3DE37F9B1C9C523E00C28031 /* k-popup-dismiss-button@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DE37F931C9C523E00C28031 /* k-popup-dismiss-button@3x.png */; }; + 3DE37F9C1C9C523E00C28031 /* BIZPopupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37F961C9C523E00C28031 /* BIZPopupViewController.m */; }; + 3DE37F9D1C9C523E00C28031 /* UIViewController+BIZChildViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37F991C9C523E00C28031 /* UIViewController+BIZChildViewController.m */; }; + 3DE37FA71C9C526800C28031 /* TPKeyboardAvoidingCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37FA01C9C526800C28031 /* TPKeyboardAvoidingCollectionView.m */; }; + 3DE37FA81C9C526800C28031 /* TPKeyboardAvoidingScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37FA21C9C526800C28031 /* TPKeyboardAvoidingScrollView.m */; }; + 3DE37FA91C9C526800C28031 /* TPKeyboardAvoidingTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37FA41C9C526800C28031 /* TPKeyboardAvoidingTableView.m */; }; + 3DE37FAA1C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE37FA61C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3D1157F91C456919009A232B /* Culqi-PruebaMovil.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Culqi-PruebaMovil.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D1157FD1C456919009A232B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 3D1157FF1C456919009A232B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 3D1158001C456919009A232B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 3D1158021C456919009A232B /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 3D1158031C456919009A232B /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 3D1158061C456919009A232B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 3D1158081C456919009A232B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3D11580B1C456919009A232B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 3D11580D1C456919009A232B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3D1158131C45C073009A232B /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 3D928EF41C48021800611982 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3D928EF61C48021800611982 /* JSONModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONModel.h; sourceTree = ""; }; + 3D928EF71C48021800611982 /* JSONModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONModel.m; sourceTree = ""; }; + 3D928EF81C48021800611982 /* JSONModelArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONModelArray.h; sourceTree = ""; }; + 3D928EF91C48021800611982 /* JSONModelArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONModelArray.m; sourceTree = ""; }; + 3D928EFA1C48021800611982 /* JSONModelClassProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONModelClassProperty.h; sourceTree = ""; }; + 3D928EFB1C48021800611982 /* JSONModelClassProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONModelClassProperty.m; sourceTree = ""; }; + 3D928EFC1C48021800611982 /* JSONModelError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONModelError.h; sourceTree = ""; }; + 3D928EFD1C48021800611982 /* JSONModelError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONModelError.m; sourceTree = ""; }; + 3D928EFF1C48021800611982 /* NSArray+JSONModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+JSONModel.h"; sourceTree = ""; }; + 3D928F001C48021800611982 /* NSArray+JSONModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+JSONModel.m"; sourceTree = ""; }; + 3D928F011C48021800611982 /* JSONModelLib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONModelLib.h; sourceTree = ""; }; + 3D928F031C48021800611982 /* JSONAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONAPI.h; sourceTree = ""; }; + 3D928F041C48021800611982 /* JSONAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONAPI.m; sourceTree = ""; }; + 3D928F051C48021800611982 /* JSONHTTPClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONHTTPClient.h; sourceTree = ""; }; + 3D928F061C48021800611982 /* JSONHTTPClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONHTTPClient.m; sourceTree = ""; }; + 3D928F071C48021800611982 /* JSONModel+networking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSONModel+networking.h"; sourceTree = ""; }; + 3D928F081C48021800611982 /* JSONModel+networking.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSONModel+networking.m"; sourceTree = ""; }; + 3D928F0A1C48021800611982 /* JSONKeyMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONKeyMapper.h; sourceTree = ""; }; + 3D928F0B1C48021800611982 /* JSONKeyMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONKeyMapper.m; sourceTree = ""; }; + 3D928F0C1C48021800611982 /* JSONValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONValueTransformer.h; sourceTree = ""; }; + 3D928F0D1C48021800611982 /* JSONValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONValueTransformer.m; sourceTree = ""; }; + 3D928F191C48022700611982 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 3D928F1C1C4808AF00611982 /* CrearVenta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrearVenta.h; sourceTree = ""; }; + 3D928F1D1C4808AF00611982 /* CrearVenta.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CrearVenta.m; sourceTree = ""; }; + 3DE37F921C9C523E00C28031 /* k-popup-dismiss-button@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "k-popup-dismiss-button@2x.png"; sourceTree = ""; }; + 3DE37F931C9C523E00C28031 /* k-popup-dismiss-button@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "k-popup-dismiss-button@3x.png"; sourceTree = ""; }; + 3DE37F951C9C523E00C28031 /* BIZPopupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BIZPopupViewController.h; sourceTree = ""; }; + 3DE37F961C9C523E00C28031 /* BIZPopupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BIZPopupViewController.m; sourceTree = ""; }; + 3DE37F971C9C523E00C28031 /* BIZPopupViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BIZPopupViewControllerDelegate.h; sourceTree = ""; }; + 3DE37F981C9C523E00C28031 /* UIViewController+BIZChildViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+BIZChildViewController.h"; sourceTree = ""; }; + 3DE37F991C9C523E00C28031 /* UIViewController+BIZChildViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+BIZChildViewController.m"; sourceTree = ""; }; + 3DE37F9F1C9C526800C28031 /* TPKeyboardAvoidingCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingCollectionView.h; sourceTree = ""; }; + 3DE37FA01C9C526800C28031 /* TPKeyboardAvoidingCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingCollectionView.m; sourceTree = ""; }; + 3DE37FA11C9C526800C28031 /* TPKeyboardAvoidingScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingScrollView.h; sourceTree = ""; }; + 3DE37FA21C9C526800C28031 /* TPKeyboardAvoidingScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingScrollView.m; sourceTree = ""; }; + 3DE37FA31C9C526800C28031 /* TPKeyboardAvoidingTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingTableView.h; sourceTree = ""; }; + 3DE37FA41C9C526800C28031 /* TPKeyboardAvoidingTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingTableView.m; sourceTree = ""; }; + 3DE37FA51C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+TPKeyboardAvoidingAdditions.h"; sourceTree = ""; }; + 3DE37FA61C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+TPKeyboardAvoidingAdditions.m"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3D1157F61C456919009A232B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D928F1A1C48022700611982 /* SystemConfiguration.framework in Frameworks */, + 3D1158141C45C073009A232B /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3D1157F01C456919009A232B = { + isa = PBXGroup; + children = ( + 3D928F191C48022700611982 /* SystemConfiguration.framework */, + 3D928EF31C48021800611982 /* JSONModel */, + 3D1158131C45C073009A232B /* WebKit.framework */, + 3D1157FB1C456919009A232B /* Culqi-PruebaMovil */, + 3D1157FA1C456919009A232B /* Products */, + ); + sourceTree = ""; + }; + 3D1157FA1C456919009A232B /* Products */ = { + isa = PBXGroup; + children = ( + 3D1157F91C456919009A232B /* Culqi-PruebaMovil.app */, + ); + name = Products; + sourceTree = ""; + }; + 3D1157FB1C456919009A232B /* Culqi-PruebaMovil */ = { + isa = PBXGroup; + children = ( + 3DE37F9E1C9C526800C28031 /* TPKeyboardAvoiding */, + 3DE37F911C9C523E00C28031 /* Resources */, + 3DE37F941C9C523E00C28031 /* Classes */, + 3D928F1C1C4808AF00611982 /* CrearVenta.h */, + 3D928F1D1C4808AF00611982 /* CrearVenta.m */, + 3D1157FF1C456919009A232B /* AppDelegate.h */, + 3D1158001C456919009A232B /* AppDelegate.m */, + 3D1158021C456919009A232B /* ViewController.h */, + 3D1158031C456919009A232B /* ViewController.m */, + 3D1158051C456919009A232B /* Main.storyboard */, + 3D1158081C456919009A232B /* Assets.xcassets */, + 3D11580A1C456919009A232B /* LaunchScreen.storyboard */, + 3D11580D1C456919009A232B /* Info.plist */, + 3D1157FC1C456919009A232B /* Supporting Files */, + ); + path = "Culqi-PruebaMovil"; + sourceTree = ""; + }; + 3D1157FC1C456919009A232B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 3D1157FD1C456919009A232B /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 3D928EF31C48021800611982 /* JSONModel */ = { + isa = PBXGroup; + children = ( + 3D928EF41C48021800611982 /* Info.plist */, + 3D928EF51C48021800611982 /* JSONModel */, + 3D928EFE1C48021800611982 /* JSONModelCategories */, + 3D928F011C48021800611982 /* JSONModelLib.h */, + 3D928F021C48021800611982 /* JSONModelNetworking */, + 3D928F091C48021800611982 /* JSONModelTransformations */, + ); + path = JSONModel; + sourceTree = ""; + }; + 3D928EF51C48021800611982 /* JSONModel */ = { + isa = PBXGroup; + children = ( + 3D928EF61C48021800611982 /* JSONModel.h */, + 3D928EF71C48021800611982 /* JSONModel.m */, + 3D928EF81C48021800611982 /* JSONModelArray.h */, + 3D928EF91C48021800611982 /* JSONModelArray.m */, + 3D928EFA1C48021800611982 /* JSONModelClassProperty.h */, + 3D928EFB1C48021800611982 /* JSONModelClassProperty.m */, + 3D928EFC1C48021800611982 /* JSONModelError.h */, + 3D928EFD1C48021800611982 /* JSONModelError.m */, + ); + path = JSONModel; + sourceTree = ""; + }; + 3D928EFE1C48021800611982 /* JSONModelCategories */ = { + isa = PBXGroup; + children = ( + 3D928EFF1C48021800611982 /* NSArray+JSONModel.h */, + 3D928F001C48021800611982 /* NSArray+JSONModel.m */, + ); + path = JSONModelCategories; + sourceTree = ""; + }; + 3D928F021C48021800611982 /* JSONModelNetworking */ = { + isa = PBXGroup; + children = ( + 3D928F031C48021800611982 /* JSONAPI.h */, + 3D928F041C48021800611982 /* JSONAPI.m */, + 3D928F051C48021800611982 /* JSONHTTPClient.h */, + 3D928F061C48021800611982 /* JSONHTTPClient.m */, + 3D928F071C48021800611982 /* JSONModel+networking.h */, + 3D928F081C48021800611982 /* JSONModel+networking.m */, + ); + path = JSONModelNetworking; + sourceTree = ""; + }; + 3D928F091C48021800611982 /* JSONModelTransformations */ = { + isa = PBXGroup; + children = ( + 3D928F0A1C48021800611982 /* JSONKeyMapper.h */, + 3D928F0B1C48021800611982 /* JSONKeyMapper.m */, + 3D928F0C1C48021800611982 /* JSONValueTransformer.h */, + 3D928F0D1C48021800611982 /* JSONValueTransformer.m */, + ); + path = JSONModelTransformations; + sourceTree = ""; + }; + 3DE37F911C9C523E00C28031 /* Resources */ = { + isa = PBXGroup; + children = ( + 3DE37F921C9C523E00C28031 /* k-popup-dismiss-button@2x.png */, + 3DE37F931C9C523E00C28031 /* k-popup-dismiss-button@3x.png */, + ); + path = Resources; + sourceTree = ""; + }; + 3DE37F941C9C523E00C28031 /* Classes */ = { + isa = PBXGroup; + children = ( + 3DE37F951C9C523E00C28031 /* BIZPopupViewController.h */, + 3DE37F961C9C523E00C28031 /* BIZPopupViewController.m */, + 3DE37F971C9C523E00C28031 /* BIZPopupViewControllerDelegate.h */, + 3DE37F981C9C523E00C28031 /* UIViewController+BIZChildViewController.h */, + 3DE37F991C9C523E00C28031 /* UIViewController+BIZChildViewController.m */, + ); + path = Classes; + sourceTree = ""; + }; + 3DE37F9E1C9C526800C28031 /* TPKeyboardAvoiding */ = { + isa = PBXGroup; + children = ( + 3DE37F9F1C9C526800C28031 /* TPKeyboardAvoidingCollectionView.h */, + 3DE37FA01C9C526800C28031 /* TPKeyboardAvoidingCollectionView.m */, + 3DE37FA11C9C526800C28031 /* TPKeyboardAvoidingScrollView.h */, + 3DE37FA21C9C526800C28031 /* TPKeyboardAvoidingScrollView.m */, + 3DE37FA31C9C526800C28031 /* TPKeyboardAvoidingTableView.h */, + 3DE37FA41C9C526800C28031 /* TPKeyboardAvoidingTableView.m */, + 3DE37FA51C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.h */, + 3DE37FA61C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.m */, + ); + path = TPKeyboardAvoiding; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3D1157F81C456919009A232B /* Culqi-PruebaMovil */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D1158101C456919009A232B /* Build configuration list for PBXNativeTarget "Culqi-PruebaMovil" */; + buildPhases = ( + 3D1157F51C456919009A232B /* Sources */, + 3D1157F61C456919009A232B /* Frameworks */, + 3D1157F71C456919009A232B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Culqi-PruebaMovil"; + productName = "Culqi-PruebaMovil"; + productReference = 3D1157F91C456919009A232B /* Culqi-PruebaMovil.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3D1157F11C456919009A232B /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = Culqi; + TargetAttributes = { + 3D1157F81C456919009A232B = { + CreatedOnToolsVersion = 7.2; + }; + }; + }; + buildConfigurationList = 3D1157F41C456919009A232B /* Build configuration list for PBXProject "Culqi-PruebaMovil" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3D1157F01C456919009A232B; + productRefGroup = 3D1157FA1C456919009A232B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3D1157F81C456919009A232B /* Culqi-PruebaMovil */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3D1157F71C456919009A232B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D928F0E1C48021800611982 /* Info.plist in Resources */, + 3D11580C1C456919009A232B /* LaunchScreen.storyboard in Resources */, + 3D1158091C456919009A232B /* Assets.xcassets in Resources */, + 3DE37F9A1C9C523E00C28031 /* k-popup-dismiss-button@2x.png in Resources */, + 3D1158071C456919009A232B /* Main.storyboard in Resources */, + 3DE37F9B1C9C523E00C28031 /* k-popup-dismiss-button@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3D1157F51C456919009A232B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D928F121C48021800611982 /* JSONModelError.m in Sources */, + 3DE37FA71C9C526800C28031 /* TPKeyboardAvoidingCollectionView.m in Sources */, + 3D928F131C48021800611982 /* NSArray+JSONModel.m in Sources */, + 3D928F111C48021800611982 /* JSONModelClassProperty.m in Sources */, + 3D1158041C456919009A232B /* ViewController.m in Sources */, + 3DE37F9D1C9C523E00C28031 /* UIViewController+BIZChildViewController.m in Sources */, + 3D928F1E1C4808AF00611982 /* CrearVenta.m in Sources */, + 3D928F171C48021800611982 /* JSONKeyMapper.m in Sources */, + 3D1158011C456919009A232B /* AppDelegate.m in Sources */, + 3DE37FAA1C9C526800C28031 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */, + 3D928F101C48021800611982 /* JSONModelArray.m in Sources */, + 3D928F161C48021800611982 /* JSONModel+networking.m in Sources */, + 3D1157FE1C456919009A232B /* main.m in Sources */, + 3DE37FA91C9C526800C28031 /* TPKeyboardAvoidingTableView.m in Sources */, + 3D928F141C48021800611982 /* JSONAPI.m in Sources */, + 3DE37F9C1C9C523E00C28031 /* BIZPopupViewController.m in Sources */, + 3D928F181C48021800611982 /* JSONValueTransformer.m in Sources */, + 3DE37FA81C9C526800C28031 /* TPKeyboardAvoidingScrollView.m in Sources */, + 3D928F151C48021800611982 /* JSONHTTPClient.m in Sources */, + 3D928F0F1C48021800611982 /* JSONModel.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3D1158051C456919009A232B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3D1158061C456919009A232B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 3D11580A1C456919009A232B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3D11580B1C456919009A232B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3D11580E1C456919009A232B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3D11580F1C456919009A232B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3D1158111C456919009A232B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "Culqi-PruebaMovil/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "Culqi.Culqi-PruebaMovil"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3D1158121C456919009A232B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "Culqi-PruebaMovil/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "Culqi.Culqi-PruebaMovil"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3D1157F41C456919009A232B /* Build configuration list for PBXProject "Culqi-PruebaMovil" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D11580E1C456919009A232B /* Debug */, + 3D11580F1C456919009A232B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D1158101C456919009A232B /* Build configuration list for PBXNativeTarget "Culqi-PruebaMovil" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D1158111C456919009A232B /* Debug */, + 3D1158121C456919009A232B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3D1157F11C456919009A232B /* Project object */; +} diff --git a/Culqi-PruebaMovil.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Culqi-PruebaMovil.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..6630d38 --- /dev/null +++ b/Culqi-PruebaMovil.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Culqi-PruebaMovil.xcodeproj/project.xcworkspace/xcuserdata/William.xcuserdatad/UserInterfaceState.xcuserstate b/Culqi-PruebaMovil.xcodeproj/project.xcworkspace/xcuserdata/William.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..c15c8b5552109640bde778dbe5b255a5d1e2724c GIT binary patch literal 45768 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 literal 0 HcmV?d00001 diff --git a/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/Culqi-PruebaMovil.xcscheme b/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/Culqi-PruebaMovil.xcscheme new file mode 100644 index 0000000..d0a55f0 --- /dev/null +++ b/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/Culqi-PruebaMovil.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/xcschememanagement.plist b/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..4ebc6bd --- /dev/null +++ b/Culqi-PruebaMovil.xcodeproj/xcuserdata/William.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Culqi-PruebaMovil.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + 3D1157F81C456919009A232B + + primary + + + + + diff --git a/Culqi-PruebaMovil/AppDelegate.h b/Culqi-PruebaMovil/AppDelegate.h new file mode 100644 index 0000000..53c98aa --- /dev/null +++ b/Culqi-PruebaMovil/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// Culqi-PruebaMovil +// +// Created by William Muro on 1/12/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/Culqi-PruebaMovil/AppDelegate.m b/Culqi-PruebaMovil/AppDelegate.m new file mode 100644 index 0000000..1f98481 --- /dev/null +++ b/Culqi-PruebaMovil/AppDelegate.m @@ -0,0 +1,45 @@ +// +// AppDelegate.m +// Culqi-PruebaMovil +// +// Created by William Muro on 1/12/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/Culqi-PruebaMovil/Assets.xcassets/AppIcon.appiconset/Contents.json b/Culqi-PruebaMovil/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..eeea76c --- /dev/null +++ b/Culqi-PruebaMovil/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Culqi-PruebaMovil/Base.lproj/LaunchScreen.storyboard b/Culqi-PruebaMovil/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..2e721e1 --- /dev/null +++ b/Culqi-PruebaMovil/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Culqi-PruebaMovil/Base.lproj/Main.storyboard b/Culqi-PruebaMovil/Base.lproj/Main.storyboard new file mode 100644 index 0000000..e77299e --- /dev/null +++ b/Culqi-PruebaMovil/Base.lproj/Main.storyboard @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Culqi-PruebaMovil/Classes/BIZPopupViewController.h b/Culqi-PruebaMovil/Classes/BIZPopupViewController.h new file mode 100644 index 0000000..a0c6529 --- /dev/null +++ b/Culqi-PruebaMovil/Classes/BIZPopupViewController.h @@ -0,0 +1,33 @@ +// +// BIZPopupViewController.h +// IgorBizi@mail.ru +// +// Created by IgorBizi@mail.ru on 10/12/15. +// Copyright © 2015 IgorBizi@mail.ru. All rights reserved. +// + +#import +#import "BIZPopupViewControllerDelegate.h" + + +@interface BIZPopupViewController : UIViewController + +//! Designated initializer +- (instancetype)initWithContentViewController:(UIViewController *)contentViewController contentSize:(CGSize)contentSize; +@property (nonatomic, weak) id delegate; +- (void)dismissPopupViewControllerAnimated:(BOOL)animated; +- (void)dismissPopupViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion; +//! Show Dismiss Button. YES is default +@property (nonatomic) BOOL showDismissButton; +//! Enable Background Fade and Popup is presented. YES is default +@property (nonatomic) BOOL enableBackgroundFade; + +//* Subclass for customization ++ (UIColor *)kColor_modalBackground; ++ (UIColor *)kColor_modalBackgroundClear; ++ (UIImage *)kImage_dismissButton; ++ (CGSize)kSize_dismissButton; ++ (CGFloat)kSideInset_dismissButton; ++ (CGFloat)kCornerRadius; + +@end diff --git a/Culqi-PruebaMovil/Classes/BIZPopupViewController.m b/Culqi-PruebaMovil/Classes/BIZPopupViewController.m new file mode 100644 index 0000000..e470a3b --- /dev/null +++ b/Culqi-PruebaMovil/Classes/BIZPopupViewController.m @@ -0,0 +1,292 @@ +// +// BIZPopupViewController.m +// IgorBizi@mail.ru +// +// Created by IgorBizi@mail.ru on 10/12/15. +// Copyright © 2015 IgorBizi@mail.ru. All rights reserved. +// + +#import "BIZPopupViewController.h" +#import +#import "UIViewController+BIZChildViewController.h" +#import "TPKeyboardAvoidingScrollView.h" + + +#define kDuration_animation 0.2 + + +@interface BIZPopupViewController () +@property (nonatomic, strong) TPKeyboardAvoidingScrollView *scrollView; +@property (nonatomic, strong) UIView *scrollContentView; + +//! Size of content +@property (nonatomic) CGSize contentSize; +//! ContentViewController +@property (nonatomic, strong) UIViewController *contentViewController; + +@property (nonatomic) BOOL lightStatusBar; +@property (nonatomic, strong) UIButton *dismissButton; +@end + + +@implementation BIZPopupViewController + + +#pragma mark - Class + + ++ (UIColor *)kColor_modalBackground +{ + return [UIColor colorWithRed:29/255.0 green:39/255.0 blue:57/255.0 alpha:0.7]; +} + ++ (UIColor *)kColor_modalBackgroundClear +{ + return [[self kColor_modalBackground] colorWithAlphaComponent:0.0]; +} + ++ (UIImage *)kImage_dismissButton +{ + return [UIImage imageNamed:@"k-popup-dismiss-butto"]; +} + ++ (CGSize)kSize_dismissButton +{ + return CGSizeMake(40.0, 40.0); +} + ++ (CGFloat)kSideInset_dismissButton +{ + return 10.0; +} + ++ (CGFloat)kCornerRadius +{ + return 4.0; +} + + +#pragma mark - LifeCycle + + +- (instancetype)initWithContentViewController:(UIViewController *)contentViewController contentSize:(CGSize)contentSize +{ + if (self = [super init]) { + + _contentViewController = contentViewController; + _contentSize = contentSize; + _showDismissButton = YES; + _enableBackgroundFade = YES; + + [self loadViewIfNeeded]; + + [self createDismissButton]; + + self.scrollView = [[TPKeyboardAvoidingScrollView alloc] initWithFrame:self.view.bounds]; + self.scrollView.contentSize = self.view.bounds.size; + self.scrollView.alwaysBounceHorizontal = NO; + self.scrollView.showsVerticalScrollIndicator = NO; + self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:self.scrollView]; + self.scrollContentView = [[UIView alloc] initWithFrame:self.scrollView.bounds]; + [self.scrollView addSubview:self.scrollContentView]; + self.view.backgroundColor = self.scrollContentView.backgroundColor = [UIColor clearColor]; + [self containerAddChildViewController:_contentViewController withRootView:self.scrollContentView]; + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + [self setupContentViewPosition]; + [self loadVisibleState:NO]; + + //scrollView + view + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; + + //scrollView + scrollContentView + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollContentView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollContentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollContentView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollContentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; + } + return self; +} + +- (void)setupContentViewPosition +{ + self.contentViewController.view.bounds = CGRectMake(0, 0, self.contentSize.width, self.contentSize.height); + self.scrollView.contentSize = self.scrollView.bounds.size; + self.contentViewController.view.center = self.view.center; + + [self.contentViewController.view layoutIfNeeded]; + [self.view setNeedsLayout]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + [self setupContentViewPosition]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self setup]; +} + +- (void)setup +{ + self.contentViewController.view.layer.cornerRadius = [[self class] kCornerRadius]; + self.contentViewController.view.layer.masksToBounds = YES; + + [self createGestureEvents]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self startPopupAppearance]; +} + +- (void)createDismissButton +{ + self.dismissButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.dismissButton setImage:[[self class] kImage_dismissButton] forState:UIControlStateNormal]; + [self.dismissButton addTarget:self action:@selector(dismissPopupViewControllerAnimatedPassiveAction) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.dismissButton]; + + self.dismissButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:1.0 constant:[[self class] kSize_dismissButton].width]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:1.0 constant:[[self class] kSize_dismissButton].height]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:3*[[self class] kSideInset_dismissButton]]]; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-[[self class] kSideInset_dismissButton]]]; +} + +- (void)createGestureEvents +{ + UITapGestureRecognizer *gestureRecognizerPopupContentView = [[UITapGestureRecognizer alloc] initWithTarget:self action:nil]; + gestureRecognizerPopupContentView.numberOfTouchesRequired = 1; + [self.contentViewController.view addGestureRecognizer:gestureRecognizerPopupContentView]; + [gestureRecognizerPopupContentView setCancelsTouchesInView:NO]; + + UITapGestureRecognizer *gestureRecognizerView = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissPopupViewControllerAnimatedPassiveAction)]; + gestureRecognizerView.numberOfTouchesRequired = 1; + [gestureRecognizerView requireGestureRecognizerToFail:gestureRecognizerPopupContentView]; + [self.view addGestureRecognizer:gestureRecognizerView]; + [gestureRecognizerPopupContentView setCancelsTouchesInView:NO]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return self.lightStatusBar? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; +} + + +#pragma mark - Methods + + +- (void)setLightStatusBar:(BOOL)lightStatusBar +{ + _lightStatusBar = lightStatusBar; + [self setNeedsStatusBarAppearanceUpdate]; +} + +- (void)setShowDismissButton:(BOOL)showDismissButton +{ + _showDismissButton = showDismissButton; + self.dismissButton.layer.hidden = !showDismissButton; +} + + +#pragma mark - Events + + +- (void)dismissPopupViewControllerAnimatedPassiveAction +{ + [self dismissPopupViewControllerAnimated:YES]; +} + +- (void)startPopupAppearance +{ + [UIView animateWithDuration:kDuration_animation + delay:0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + + [self loadVisibleState:YES]; + } completion:nil]; +} + +- (void)dismissPopupViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion +{ + if (self.delegate && [self.delegate respondsToSelector:@selector(popupViewControllerWillDismiss)]) { + [self.delegate popupViewControllerWillDismiss]; + } + + [UIView animateWithDuration:animated? kDuration_animation : 0.0 + delay:0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + + [self loadVisibleState:NO]; + } completion:^(BOOL finished) { + + [self containerRemoveChildViewController:self.contentViewController]; + [self.presentingViewController dismissViewControllerAnimated:NO completion:^{ + + if (self.delegate && [self.delegate respondsToSelector:@selector(popupViewControllerDidDismiss)]) { + [self.delegate popupViewControllerDidDismiss]; + } + + if (completion) { + completion(); + } + }]; + }]; +} + +- (void)dismissPopupViewControllerAnimated:(BOOL)animated +{ + [self dismissPopupViewControllerAnimated:animated completion:nil]; +} + +- (void)loadVisibleState:(BOOL)visible +{ + if (visible) { + for (UIView *i in [self subviews]) + { + i.alpha = 1.0; + } + self.lightStatusBar = YES; + if (self.enableBackgroundFade) { + self.view.backgroundColor = [[self class] kColor_modalBackground]; + } + self.contentViewController.view.transform = CGAffineTransformIdentity; + + } else { + for (UIView *i in [self subviews]) + { + i.alpha = 0.0; + } + self.lightStatusBar = NO; + if (self.enableBackgroundFade) { + self.view.backgroundColor = [[self class] kColor_modalBackgroundClear]; + } + self.contentViewController.view.transform = CGAffineTransformMakeScale(0.001, 0.001); + } +} + +- (NSArray *)subviews +{ + return @[ self.dismissButton, self.contentViewController.view ]; +} + + +#pragma mark - Rotation Delegate + + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [self.view setNeedsLayout]; +} + +@end diff --git a/Culqi-PruebaMovil/Classes/BIZPopupViewControllerDelegate.h b/Culqi-PruebaMovil/Classes/BIZPopupViewControllerDelegate.h new file mode 100644 index 0000000..4e97b4d --- /dev/null +++ b/Culqi-PruebaMovil/Classes/BIZPopupViewControllerDelegate.h @@ -0,0 +1,16 @@ +// +// BIZPopupViewControllerDelegate.h +// IgorBizi@mail.ru +// +// Created by IgorBizi@mail.ru on 11/27/15. +// Copyright © 2015 IgorBizi@mail.ru. All rights reserved. +// + +#import + + +@protocol BIZPopupViewControllerDelegate +@optional +- (void)popupViewControllerWillDismiss; +- (void)popupViewControllerDidDismiss; +@end diff --git a/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.h b/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.h new file mode 100644 index 0000000..500ec04 --- /dev/null +++ b/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.h @@ -0,0 +1,16 @@ +// +// UIViewController+BIZChildViewController.h +// IgorBizi@mail.ru +// +// Created by IgorBizi@mail.ru on 11/5/15. +// Copyright © 2015 IgorBizi@mail.ru. All rights reserved. +// + +#import + + +@interface UIViewController (BIZChildViewController) +- (void)containerAddChildViewController:(UIViewController *)childViewController; +- (void)containerAddChildViewController:(UIViewController *)childViewController withRootView:(UIView *)rootView; +- (void)containerRemoveChildViewController:(UIViewController *)childViewController; +@end \ No newline at end of file diff --git a/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.m b/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.m new file mode 100644 index 0000000..7a19ccb --- /dev/null +++ b/Culqi-PruebaMovil/Classes/UIViewController+BIZChildViewController.m @@ -0,0 +1,36 @@ +// +// UIViewController+BIZChildViewController.m +// IgorBizi@mail.ru +// +// Created by IgorBizi@mail.ru on 11/5/15. +// Copyright © 2015 IgorBizi@mail.ru. All rights reserved. +// + +#import "UIViewController+BIZChildViewController.h" + + +@implementation UIViewController (BIZChildViewController) + + +- (void)containerAddChildViewController:(UIViewController *)childViewController +{ + [self addChildViewController:childViewController]; + [self.view addSubview:childViewController.view]; + [childViewController didMoveToParentViewController:self]; +} + +- (void)containerAddChildViewController:(UIViewController *)childViewController withRootView:(UIView *)rootView +{ + [self addChildViewController:childViewController]; + [rootView addSubview:childViewController.view]; + [childViewController didMoveToParentViewController:self]; +} + +- (void)containerRemoveChildViewController:(UIViewController *)childViewController +{ + [childViewController willMoveToParentViewController:nil]; + [childViewController.view removeFromSuperview]; + [childViewController removeFromParentViewController]; +} + +@end \ No newline at end of file diff --git a/Culqi-PruebaMovil/CrearVenta.h b/Culqi-PruebaMovil/CrearVenta.h new file mode 100644 index 0000000..814a620 --- /dev/null +++ b/Culqi-PruebaMovil/CrearVenta.h @@ -0,0 +1,18 @@ +// +// CrearVenta.h +// Culqi-PruebaMovil +// +// Created by William Muro on 1/14/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import +#import "JSONModel.h" + +@interface CrearVenta : JSONModel + +@property (strong, nonatomic) NSString* respuesta; +@property (strong, nonatomic) NSString* mensaje_respuesta; +@property (strong, nonatomic) NSString* informacion_venta; + +@end diff --git a/Culqi-PruebaMovil/CrearVenta.m b/Culqi-PruebaMovil/CrearVenta.m new file mode 100644 index 0000000..a8cb828 --- /dev/null +++ b/Culqi-PruebaMovil/CrearVenta.m @@ -0,0 +1,13 @@ +// +// CrearVenta.m +// Culqi-PruebaMovil +// +// Created by William Muro on 1/14/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import "CrearVenta.h" + +@implementation CrearVenta + +@end diff --git a/Culqi-PruebaMovil/Info.plist b/Culqi-PruebaMovil/Info.plist new file mode 100644 index 0000000..40c6215 --- /dev/null +++ b/Culqi-PruebaMovil/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Culqi-PruebaMovil/Resources/k-popup-dismiss-button@2x.png b/Culqi-PruebaMovil/Resources/k-popup-dismiss-button@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7691edb4c50ca416636e37b7ef9036b6412b443a GIT binary patch literal 6252 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 literal 0 HcmV?d00001 diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h new file mode 100644 index 0000000..fbd8619 --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h @@ -0,0 +1,15 @@ +// +// TPKeyboardAvoidingCollectionView.h +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved. +// + +#import +#import "UIScrollView+TPKeyboardAvoidingAdditions.h" + +@interface TPKeyboardAvoidingCollectionView : UICollectionView +- (BOOL)focusNextTextField; +- (void)scrollToActiveTextField; +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m new file mode 100644 index 0000000..68fe500 --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m @@ -0,0 +1,112 @@ +// +// TPKeyboardAvoidingCollectionView.m +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved. +// + +#import "TPKeyboardAvoidingCollectionView.h" + +@interface TPKeyboardAvoidingCollectionView () +@end + +@implementation TPKeyboardAvoidingCollectionView + +#pragma mark - Setup/Teardown + +- (void)setup { + if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; +} + +-(id)initWithFrame:(CGRect)frame { + if ( !(self = [super initWithFrame:frame]) ) return nil; + [self setup]; + return self; +} + +- (id)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { + if ( !(self = [super initWithFrame:frame collectionViewLayout:layout]) ) return nil; + [self setup]; + return self; +} + +-(void)awakeFromNib { + [self setup]; +} + +-(void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + + +-(BOOL)hasAutomaticKeyboardAvoidingBehaviour { + if ( [[[UIDevice currentDevice] systemVersion] integerValue] >= 9 + && [self.delegate isKindOfClass:[UICollectionViewController class]] ) { + // Theory: It looks like iOS 9's collection views automatically avoid the keyboard. As usual + // Apple have totally failed to document this anywhere, so this is just a guess. + return YES; + } + + return NO; +} + +-(void)setFrame:(CGRect)frame { + [super setFrame:frame]; + [self TPKeyboardAvoiding_updateContentInset]; +} + +-(void)setContentSize:(CGSize)contentSize { + if (CGSizeEqualToSize(contentSize, self.contentSize)) { + // Prevent triggering contentSize when it's already the same that + // cause weird infinte scrolling and locking bug + return; + } + [super setContentSize:contentSize]; + [self TPKeyboardAvoiding_updateContentInset]; +} + +- (BOOL)focusNextTextField { + return [self TPKeyboardAvoiding_focusNextTextField]; + +} +- (void)scrollToActiveTextField { + return [self TPKeyboardAvoiding_scrollToActiveTextField]; +} + +#pragma mark - Responders, events + +-(void)willMoveToSuperview:(UIView *)newSuperview { + [super willMoveToSuperview:newSuperview]; + if ( !newSuperview ) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + } +} + +- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; + [super touchesEnded:touches withEvent:event]; +} + +-(BOOL)textFieldShouldReturn:(UITextField *)textField { + if ( ![self focusNextTextField] ) { + [textField resignFirstResponder]; + } + return YES; +} + +-(void)layoutSubviews { + [super layoutSubviews]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; +} + +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h new file mode 100644 index 0000000..6947992 --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h @@ -0,0 +1,16 @@ +// +// TPKeyboardAvoidingScrollView.h +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import +#import "UIScrollView+TPKeyboardAvoidingAdditions.h" + +@interface TPKeyboardAvoidingScrollView : UIScrollView +- (void)contentSizeToFit; +- (BOOL)focusNextTextField; +- (void)scrollToActiveTextField; +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m new file mode 100644 index 0000000..81f753a --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m @@ -0,0 +1,91 @@ +// +// TPKeyboardAvoidingScrollView.m +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import "TPKeyboardAvoidingScrollView.h" + +@interface TPKeyboardAvoidingScrollView () +@end + +@implementation TPKeyboardAvoidingScrollView + +#pragma mark - Setup/Teardown + +- (void)setup { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; +} + +-(id)initWithFrame:(CGRect)frame { + if ( !(self = [super initWithFrame:frame]) ) return nil; + [self setup]; + return self; +} + +-(void)awakeFromNib { + [self setup]; +} + +-(void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + +-(void)setFrame:(CGRect)frame { + [super setFrame:frame]; + [self TPKeyboardAvoiding_updateContentInset]; +} + +-(void)setContentSize:(CGSize)contentSize { + [super setContentSize:contentSize]; + [self TPKeyboardAvoiding_updateFromContentSizeChange]; +} + +- (void)contentSizeToFit { + self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames]; +} + +- (BOOL)focusNextTextField { + return [self TPKeyboardAvoiding_focusNextTextField]; + +} +- (void)scrollToActiveTextField { + return [self TPKeyboardAvoiding_scrollToActiveTextField]; +} + +#pragma mark - Responders, events + +-(void)willMoveToSuperview:(UIView *)newSuperview { + [super willMoveToSuperview:newSuperview]; + if ( !newSuperview ) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + } +} + +- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; + [super touchesEnded:touches withEvent:event]; +} + +-(BOOL)textFieldShouldReturn:(UITextField *)textField { + if ( ![self focusNextTextField] ) { + [textField resignFirstResponder]; + } + return YES; +} + +-(void)layoutSubviews { + [super layoutSubviews]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; +} + +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h new file mode 100644 index 0000000..7e05a0e --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h @@ -0,0 +1,15 @@ +// +// TPKeyboardAvoidingTableView.h +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import +#import "UIScrollView+TPKeyboardAvoidingAdditions.h" + +@interface TPKeyboardAvoidingTableView : UITableView +- (BOOL)focusNextTextField; +- (void)scrollToActiveTextField; +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m new file mode 100644 index 0000000..cc652de --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m @@ -0,0 +1,116 @@ +// +// TPKeyboardAvoidingTableView.m +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import "TPKeyboardAvoidingTableView.h" + +@interface TPKeyboardAvoidingTableView () +@end + +@implementation TPKeyboardAvoidingTableView + +#pragma mark - Setup/Teardown + +- (void)setup { + if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; +} + +-(id)initWithFrame:(CGRect)frame { + if ( !(self = [super initWithFrame:frame]) ) return nil; + [self setup]; + return self; +} + +-(id)initWithFrame:(CGRect)frame style:(UITableViewStyle)withStyle { + if ( !(self = [super initWithFrame:frame style:withStyle]) ) return nil; + [self setup]; + return self; +} + +-(void)awakeFromNib { + [self setup]; +} + +-(void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + +-(BOOL)hasAutomaticKeyboardAvoidingBehaviour { + if ( [self.delegate isKindOfClass:[UITableViewController class]] ) { + // Theory: Apps built using the iOS 8.3 SDK (probably: older SDKs not tested) seem to handle keyboard + // avoiding automatically with UITableViewController. This doesn't seem to be documented anywhere + // by Apple, so results obtained only empirically. + return YES; + } + + return NO; +} + +-(void)setFrame:(CGRect)frame { + [super setFrame:frame]; + if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return; + [self TPKeyboardAvoiding_updateContentInset]; +} + +-(void)setContentSize:(CGSize)contentSize { + if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) { + [super setContentSize:contentSize]; + return; + } + if (CGSizeEqualToSize(contentSize, self.contentSize)) { + // Prevent triggering contentSize when it's already the same + // this cause table view to scroll to top on contentInset changes + return; + } + [super setContentSize:contentSize]; + [self TPKeyboardAvoiding_updateContentInset]; +} + +- (BOOL)focusNextTextField { + return [self TPKeyboardAvoiding_focusNextTextField]; + +} +- (void)scrollToActiveTextField { + return [self TPKeyboardAvoiding_scrollToActiveTextField]; +} + +#pragma mark - Responders, events + +-(void)willMoveToSuperview:(UIView *)newSuperview { + [super willMoveToSuperview:newSuperview]; + if ( !newSuperview ) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + } +} + +- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; + [super touchesEnded:touches withEvent:event]; +} + +-(BOOL)textFieldShouldReturn:(UITextField *)textField { + if ( ![self focusNextTextField] ) { + [textField resignFirstResponder]; + } + return YES; +} + +-(void)layoutSubviews { + [super layoutSubviews]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; + [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; +} + +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h b/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h new file mode 100644 index 0000000..80b9b10 --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h @@ -0,0 +1,22 @@ +// +// UIScrollView+TPKeyboardAvoidingAdditions.h +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import + +@interface UIScrollView (TPKeyboardAvoidingAdditions) +- (BOOL)TPKeyboardAvoiding_focusNextTextField; +- (void)TPKeyboardAvoiding_scrollToActiveTextField; + +- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification; +- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification; +- (void)TPKeyboardAvoiding_updateContentInset; +- (void)TPKeyboardAvoiding_updateFromContentSizeChange; +- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view; +- (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view; +-(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames; +@end diff --git a/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m b/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m new file mode 100644 index 0000000..5ddb5cf --- /dev/null +++ b/Culqi-PruebaMovil/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m @@ -0,0 +1,363 @@ +// +// UIScrollView+TPKeyboardAvoidingAdditions.m +// TPKeyboardAvoiding +// +// Created by Michael Tyson on 30/09/2013. +// Copyright 2015 A Tasty Pixel. All rights reserved. +// + +#import "UIScrollView+TPKeyboardAvoidingAdditions.h" +#import "TPKeyboardAvoidingScrollView.h" +#import + +static const CGFloat kCalculatedContentPadding = 10; +static const CGFloat kMinimumScrollOffsetPadding = 20; + +static const int kStateKey; + +#define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey") + +@interface TPKeyboardAvoidingState : NSObject +@property (nonatomic, assign) UIEdgeInsets priorInset; +@property (nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets; +@property (nonatomic, assign) BOOL keyboardVisible; +@property (nonatomic, assign) CGRect keyboardRect; +@property (nonatomic, assign) CGSize priorContentSize; +@property (nonatomic, assign) BOOL priorPagingEnabled; +@property (nonatomic, assign) BOOL ignoringNotifications; +@end + +@implementation UIScrollView (TPKeyboardAvoidingAdditions) + +- (TPKeyboardAvoidingState*)keyboardAvoidingState { + TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey); + if ( !state ) { + state = [[TPKeyboardAvoidingState alloc] init]; + objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +#if !__has_feature(objc_arc) + [state release]; +#endif + } + return state; +} + +- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification { + CGRect keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil]; + if (CGRectIsEmpty(keyboardRect)) { + return; + } + + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + + if ( state.ignoringNotifications ) { + return; + } + + state.keyboardRect = keyboardRect; + + if ( !state.keyboardVisible ) { + state.priorInset = self.contentInset; + state.priorScrollIndicatorInsets = self.scrollIndicatorInsets; + state.priorPagingEnabled = self.pagingEnabled; + } + + state.keyboardVisible = YES; + self.pagingEnabled = NO; + + if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { + state.priorContentSize = self.contentSize; + + if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) { + // Set the content size, if it's not set. Do not set content size explicitly if auto-layout + // is being used to manage subviews + self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames]; + } + } + + // Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; + [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]]; + + self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; + + UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; + if ( firstResponder ) { + CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; + [self setContentOffset:CGPointMake(self.contentOffset.x, + [self TPKeyboardAvoiding_idealOffsetForView:firstResponder + withViewingAreaHeight:viewableHeight]) + animated:NO]; + } + + self.scrollIndicatorInsets = self.contentInset; + [self layoutIfNeeded]; + + [UIView commitAnimations]; +} + +- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification { + CGRect keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil]; + if (CGRectIsEmpty(keyboardRect)) { + return; + } + + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + + if ( state.ignoringNotifications ) { + return; + } + + if ( !state.keyboardVisible ) { + return; + } + + state.keyboardRect = CGRectZero; + state.keyboardVisible = NO; + + // Restore dimensions to prior size + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; + [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]]; + + if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { + self.contentSize = state.priorContentSize; + } + + self.contentInset = state.priorInset; + self.scrollIndicatorInsets = state.priorScrollIndicatorInsets; + self.pagingEnabled = state.priorPagingEnabled; + [self layoutIfNeeded]; + [UIView commitAnimations]; +} + +- (void)TPKeyboardAvoiding_updateContentInset { + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + if ( state.keyboardVisible ) { + self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; + } +} + +- (void)TPKeyboardAvoiding_updateFromContentSizeChange { + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + if ( state.keyboardVisible ) { + state.priorContentSize = self.contentSize; + self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; + } +} + +#pragma mark - Utilities + +- (BOOL)TPKeyboardAvoiding_focusNextTextField { + UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; + if ( !firstResponder ) { + return NO; + } + + UIView *view = [self TPKeyboardAvoiding_findNextInputViewAfterView:firstResponder beneathView:self]; + + if ( view ) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), dispatch_get_main_queue(), ^{ + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + state.ignoringNotifications = YES; + [view becomeFirstResponder]; + state.ignoringNotifications = NO; + }); + return YES; + } + + return NO; +} + +-(void)TPKeyboardAvoiding_scrollToActiveTextField { + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + + if ( !state.keyboardVisible ) return; + + // Ignore any keyboard notification that occur while we scroll + // (seems to be an iOS 9 bug that causes jumping text in UITextField) + state.ignoringNotifications = YES; + + CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; + + CGPoint idealOffset + = CGPointMake(self.contentOffset.x, + [self TPKeyboardAvoiding_idealOffsetForView:[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] + withViewingAreaHeight:visibleSpace]); + + // Ordinarily we'd use -setContentOffset:animated:YES here, but it interferes with UIScrollView + // behavior which automatically ensures that the first responder is within its bounds + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self setContentOffset:idealOffset animated:YES]; + + state.ignoringNotifications = NO; + }); +} + +#pragma mark - Helpers + +- (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view { + // Search recursively for first responder + for ( UIView *childView in view.subviews ) { + if ( [childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder] ) return childView; + UIView *result = [self TPKeyboardAvoiding_findFirstResponderBeneathView:childView]; + if ( result ) return result; + } + return nil; +} + +- (UIView*)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view { + UIView * candidate = nil; + [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:view bestCandidate:&candidate]; + return candidate; +} + +- (void)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view bestCandidate:(UIView**)bestCandidate { + // Search recursively for input view below/to right of priorTextField + CGRect priorFrame = [self convertRect:priorView.frame fromView:priorView.superview]; + CGRect candidateFrame = *bestCandidate ? [self convertRect:(*bestCandidate).frame fromView:(*bestCandidate).superview] : CGRectZero; + CGFloat bestCandidateHeuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:candidateFrame]; + + for ( UIView *childView in view.subviews ) { + if ( [self TPKeyboardAvoiding_viewIsValidKeyViewCandidate:childView] ) { + CGRect frame = [self convertRect:childView.frame fromView:view]; + + // Use a heuristic to evaluate candidates + CGFloat heuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:frame]; + + // Find views beneath, or to the right. For those views that match, choose the view closest to the top left + if ( childView != priorView + && ((fabs(CGRectGetMinY(frame) - CGRectGetMinY(priorFrame)) < FLT_EPSILON && CGRectGetMinX(frame) > CGRectGetMinX(priorFrame)) + || CGRectGetMinY(frame) > CGRectGetMinY(priorFrame)) + && (!*bestCandidate || heuristic > bestCandidateHeuristic) ) { + + *bestCandidate = childView; + bestCandidateHeuristic = heuristic; + } + } else { + [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:childView bestCandidate:bestCandidate]; + } + } +} + +- (CGFloat)TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:(CGRect)frame { + return (-frame.origin.y * 1000.0) // Prefer elements closest to top (most important) + + (-frame.origin.x); // Prefer elements closest to left +} + +- (BOOL)TPKeyboardAvoiding_viewHiddenOrUserInteractionNotEnabled:(UIView *)view { + while ( view ) { + if ( view.hidden || !view.userInteractionEnabled ) { + return YES; + } + view = view.superview; + } + return NO; +} + +- (BOOL)TPKeyboardAvoiding_viewIsValidKeyViewCandidate:(UIView *)view { + if ( [self TPKeyboardAvoiding_viewHiddenOrUserInteractionNotEnabled:view] ) return NO; + + if ( [view isKindOfClass:[UITextField class]] && ((UITextField*)view).enabled ) { + return YES; + } + + if ( [view isKindOfClass:[UITextView class]] && ((UITextView*)view).isEditable ) { + return YES; + } + + return NO; +} + +- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view { + for ( UIView *childView in view.subviews ) { + if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) ) { + [self TPKeyboardAvoiding_initializeView:childView]; + } else { + [self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView]; + } + } +} + +-(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames { + + BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator; + BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator; + + self.showsVerticalScrollIndicator = NO; + self.showsHorizontalScrollIndicator = NO; + + CGRect rect = CGRectZero; + for ( UIView *view in self.subviews ) { + rect = CGRectUnion(rect, view.frame); + } + rect.size.height += kCalculatedContentPadding; + + self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator; + self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator; + + return rect.size; +} + + +- (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard { + TPKeyboardAvoidingState *state = self.keyboardAvoidingState; + UIEdgeInsets newInset = self.contentInset; + CGRect keyboardRect = state.keyboardRect; + newInset.bottom = keyboardRect.size.height - MAX((CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds)), 0); + return newInset; +} + +-(CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight { + CGSize contentSize = self.contentSize; + CGFloat offset = 0.0; + + CGRect subviewRect = [view convertRect:view.bounds toView:self]; + + // Attempt to center the subview in the visible space, but if that means there will be less than kMinimumScrollOffsetPadding + // pixels above the view, then substitute kMinimumScrollOffsetPadding + CGFloat padding = (viewAreaHeight - subviewRect.size.height) / 2; + if ( padding < kMinimumScrollOffsetPadding ) { + padding = kMinimumScrollOffsetPadding; + } + + // Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview. + // If there is a top contentInset, also compensate for this so that subviewRect will not be placed under + // things like navigation bars. + offset = subviewRect.origin.y - padding - self.contentInset.top; + + // Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom + // inset into account, as this is manipulated to make space for the keyboard. + CGFloat maxOffset = contentSize.height - viewAreaHeight - self.contentInset.top; + if (offset > maxOffset) { + offset = maxOffset; + } + + // Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account + if ( offset < -self.contentInset.top ) { + offset = -self.contentInset.top; + } + + return offset; +} + +- (void)TPKeyboardAvoiding_initializeView:(UIView*)view { + if ( [view isKindOfClass:[UITextField class]] + && ((UITextField*)view).returnKeyType == UIReturnKeyDefault + && (![(UITextField*)view delegate] || [(UITextField*)view delegate] == (id)self) ) { + [(UITextField*)view setDelegate:(id)self]; + UIView *otherView = [self TPKeyboardAvoiding_findNextInputViewAfterView:view beneathView:self]; + + if ( otherView ) { + ((UITextField*)view).returnKeyType = UIReturnKeyNext; + } else { + ((UITextField*)view).returnKeyType = UIReturnKeyDone; + } + } +} + +@end + + +@implementation TPKeyboardAvoidingState +@end diff --git a/Culqi-PruebaMovil/ViewController.h b/Culqi-PruebaMovil/ViewController.h new file mode 100644 index 0000000..9edd56f --- /dev/null +++ b/Culqi-PruebaMovil/ViewController.h @@ -0,0 +1,24 @@ +// +// ViewController.h +// Culqi-PruebaMovil +// +// Created by William Muro on 1/12/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import +#import + +@interface ViewController : UIViewController + +@property (nonatomic,retain) IBOutlet UITextField *txtfName; +@property (nonatomic,retain) IBOutlet UITextField *txtfLast; +@property (nonatomic,retain) IBOutlet UITextField *txtfEmail; +@property (nonatomic,retain) IBOutlet UITextField *txtfPhone; +@property (nonatomic,retain) IBOutlet UITextField *txtfCity; +@property (nonatomic,retain) IBOutlet UITextField *txtfAddress; +@property (nonatomic,retain) IBOutlet UITextField *txtfCOuntry; + + +@end + diff --git a/Culqi-PruebaMovil/ViewController.m b/Culqi-PruebaMovil/ViewController.m new file mode 100644 index 0000000..60b2de6 --- /dev/null +++ b/Culqi-PruebaMovil/ViewController.m @@ -0,0 +1,311 @@ +// +// ViewController.m +// Culqi-PruebaMovil +// +// Created by William Muro on 1/12/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import "ViewController.h" +#import "CrearVenta.h" +#import "BIZPopupViewController.h" + +@interface ViewController () +@property (strong, nonatomic) WKWebView *webView; +@property (strong, nonatomic) NSString *informacionVenta; +@property (strong, nonatomic) NSString *numeroPedido; + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + /* + [NSURLConnection sendAsynchronousRequest:request + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + + + }]; + */ + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button setTitle:@"Pagar" forState:UIControlStateNormal]; + [button sizeToFit]; + button.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height-200); + [button addTarget:self action:@selector(buttonPressed:) + forControlEvents:UIControlEventTouchUpInside]; + + [self.view addSubview:button]; +} + + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; +} + +- (void)userContentController:(WKUserContentController *)userContentController + didReceiveScriptMessage:(WKScriptMessage *)message { + + NSLog(@"Evento recibido: %@", message.body); + + if ([message.body isEqualToString:@"checkout_cerrado"]) { + + [[self viewController] dismissViewControllerAnimated:YES completion:nil]; + + NSLog(@"Checkout Cerrado"); + + } else { + + //[[self viewController] dismissViewControllerAnimated:YES completion:nil]; + + //JSON Creation + NSDictionary *dict = @{@"respuesta" : message.body}; + + NSError *error = nil; + NSData *json; + + if ([NSJSONSerialization isValidJSONObject:dict]) + { + json = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&error]; + + if (json != nil && error == nil) + { + NSString *jsonString = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; + + NSLog(@"JSON: %@", jsonString); + } + } + + //Create Request + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setURL:[NSURL URLWithString:@"https://integ-com.culqi.com/respuesta-demo-integracion"]]; + [request setHTTPMethod:@"POST"]; + [request setHTTPBody:json]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + + + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + + [[session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; + int code = [httpResponse statusCode]; + NSLog(@"Respuesta ...%i", code); + NSLog(@"Data ...%@", data); + + if (code == 200) { + + dispatch_async(dispatch_get_main_queue(), ^{ + + [[self viewController] dismissViewControllerAnimated:YES completion:nil]; + + NSString *myString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSASCIIStringEncoding]; + + + NSLog(@"dataAsString %@", myString); + + + UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Respuesta" + message:myString + delegate:nil + cancelButtonTitle:@"Aceptar" + otherButtonTitles:nil]; + [errorAlert show]; + + + }); + } + }] resume]; + + + + + } + +} + +- (UIViewController *)viewController { + UIResponder *responder = self; + while (![responder isKindOfClass:[UIViewController class]]) { + responder = [responder nextResponder]; + if (nil == responder) { + break; + } + } + return (UIViewController *)responder; +} + +- (void)loadPopUP { + + UIViewController *checkout_view = [[UIViewController alloc] init]; + checkout_view.view.backgroundColor = [UIColor whiteColor]; + + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] + init]; + WKUserContentController *controller = [[WKUserContentController alloc] + init]; + + // Add a script handler for the "observe" call. This is added to every frame + // in the document (window.webkit.messageHandlers.NAME). + + [controller addScriptMessageHandler:self name:@"observe"]; + configuration.userContentController = controller; + + NSURL *jsbin = [NSURL URLWithString:[NSString stringWithFormat:@"https://integ-pago.culqi.com/api/v1/formulario/movil/1/demo/%@",_informacionVenta]]; + + NSLog(@"URL: %@", jsbin); + + UILabel *fromLabel = [[UILabel alloc]initWithFrame:CGRectMake(15, 10, 250, 15)]; + fromLabel.text = [NSString stringWithFormat:@"Pedido: %@", _numeroPedido]; + fromLabel.numberOfLines = 1; + fromLabel.minimumScaleFactor = 10.0f/12.0f; + fromLabel.clipsToBounds = YES; + fromLabel.font = [UIFont systemFontOfSize:12]; + fromLabel.backgroundColor = [UIColor clearColor]; + fromLabel.textColor = [UIColor darkGrayColor]; + fromLabel.textAlignment = NSTextAlignmentLeft; + + _webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 360) + configuration:configuration]; + _webView.scrollView.scrollEnabled = false; + [_webView loadRequest:[NSURLRequest requestWithURL:jsbin]]; + _webView.center = CGPointMake(300/2, _webView.center.y); + + + [checkout_view.view addSubview:_webView]; + //[checkout_view.view addSubview:fromLabel]; + + //[[self viewController] presentViewController:checkout_view animated:YES completion:nil]; + + BIZPopupViewController *popupViewController = [[BIZPopupViewController alloc] initWithContentViewController:checkout_view contentSize:CGSizeMake(300, 360)]; + [self presentViewController:popupViewController animated:NO completion:nil]; +} + + +- (void)buttonPressed:(UIButton *)button { + + _numeroPedido = [NSString stringWithFormat:@"%@",[NSDate date]]; + //JSON Creation + NSDictionary *dict = @{@"numero_pedido" : _numeroPedido, + @"nombres" : _txtfName.text, + @"apellidos" : _txtfLast.text, + @"id_usuario_comercio" : @"culqi_demo", + @"moneda" : @"PEN", + @"descripcion" : @"Venta de prueba", + @"ciudad" : _txtfCity.text, + @"pais" : _txtfCOuntry.text, + @"direccion" : _txtfAddress.text, + @"telefono" : _txtfPhone.text, + @"correo_electronico" : _txtfEmail.text, + @"monto" : @"1000"}; + + NSError *error = nil; + NSData *json; + + if ([NSJSONSerialization isValidJSONObject:dict]) + { + json = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&error]; + + if (json != nil && error == nil) + { + NSString *jsonString = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; + + NSLog(@"JSON: %@", jsonString); + } + } + + //Create Request + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setURL:[NSURL URLWithString:@"https://integ-com.culqi.com/venta/movil"]]; + [request setHTTPMethod:@"POST"]; + [request setHTTPBody:json]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + + + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + + [[session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; + int code = [httpResponse statusCode]; + NSLog(@"Respuesta ...%i", code); + + if (code == 200) { + + NSLog(@"dataAsString %@", [NSString stringWithUTF8String:[data bytes]]); + + NSError *jsonError; + + NSMutableDictionary * innerJson = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; + + NSData *jsonResponse; + NSString *jsonResponseString; + + if ([NSJSONSerialization isValidJSONObject:innerJson]) + { + jsonResponse = [NSJSONSerialization dataWithJSONObject:innerJson options:NSJSONWritingPrettyPrinted error:&error]; + + if (jsonResponse != nil && error == nil) + { + jsonResponseString = [[NSString alloc] initWithData:jsonResponse encoding:NSUTF8StringEncoding]; + + NSLog(@"JSON: %@", jsonResponseString); + } + } + + dispatch_async(dispatch_get_main_queue(), ^{ + + NSError* err = nil; + // code here + CrearVenta* creacionVenta = [[CrearVenta alloc] initWithString:jsonResponseString error:&err]; + + if(err) { + + NSLog(@"json error : %@", [jsonError localizedDescription]); + + } + + if ([creacionVenta.respuesta isEqual: @"venta_registrada"]) { + + NSLog(@"Informacion venta: %@", creacionVenta.informacion_venta); + + _informacionVenta = creacionVenta.informacion_venta; + + [self loadPopUP]; + + } else { + + UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Error" + message:creacionVenta.mensaje_respuesta + delegate:nil + cancelButtonTitle:@"Aceptar" + otherButtonTitles:nil]; + [errorAlert show]; + } + }); + + + + + + + + } + }] resume]; + + + + NSLog(@"Boton presionado ..."); + + + +} + +@end diff --git a/Culqi-PruebaMovil/main.m b/Culqi-PruebaMovil/main.m new file mode 100644 index 0000000..53efac6 --- /dev/null +++ b/Culqi-PruebaMovil/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Culqi-PruebaMovil +// +// Created by William Muro on 1/12/16. +// Copyright © 2016 Culqi. All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/JSONModel/Info.plist b/JSONModel/Info.plist new file mode 100644 index 0000000..d3de8ee --- /dev/null +++ b/JSONModel/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/JSONModel/JSONModel/JSONModel.h b/JSONModel/JSONModel/JSONModel.h new file mode 100644 index 0000000..6bcaabb --- /dev/null +++ b/JSONModel/JSONModel/JSONModel.h @@ -0,0 +1,350 @@ +// +// JSONModel.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +#import "JSONModelError.h" +#import "JSONValueTransformer.h" +#import "JSONKeyMapper.h" + +///////////////////////////////////////////////////////////////////////////////////////////// +#if TARGET_IPHONE_SIMULATOR +#define JMLog( s, ... ) NSLog( @"[%@:%d] %@", [[NSString stringWithUTF8String:__FILE__] \ +lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] ) +#else +#define JMLog( s, ... ) +#endif +///////////////////////////////////////////////////////////////////////////////////////////// + +#pragma mark - Property Protocols +/** + * Protocol for defining properties in a JSON Model class that should not be considered at all + * neither while importing nor when exporting JSON. + * + * @property (strong, nonatomic) NSString<Ignore>* propertyName; + * + */ +@protocol Ignore +@end + +/** + * Protocol for defining optional properties in a JSON Model class. Use like below to define + * model properties that are not required to have values in the JSON input: + * + * @property (strong, nonatomic) NSString<Optional>* propertyName; + * + */ +@protocol Optional +@end + +/** + * Protocol for defining index properties in a JSON Model class. Use like below to define + * model properties that are considered the Model's identifier (id). + * + * @property (strong, nonatomic) NSString<Index>* propertyName; + * + */ +@protocol Index +@end + +/** + * Make all objects Optional compatible to avoid compiler warnings + */ +@interface NSObject(JSONModelPropertyCompatibility) +@end + +/** + * ConvertOnDemand enables lazy model initialization for NSArrays of models + * + * @property (strong, nonatomic) NSArray<JSONModel, ConvertOnDemand>* propertyName; + */ +@protocol ConvertOnDemand +@end + +/** + * Make all arrays ConvertOnDemand compatible to avoid compiler warnings + */ +@interface NSArray(JSONModelPropertyCompatibility) +@end + +///////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - JSONModel protocol +/** + * A protocol describing an abstract JSONModel class + * JSONModel conforms to this protocol, so it can use itself abstractly + */ +@protocol AbstractJSONModelProtocol + +@required + /** + * All JSONModel classes should implement initWithDictionary: + * + * For most classes the default initWithDictionary: inherited from JSONModel itself + * should suffice, but developers have the option ot also overwrite it if needed. + * + * @param dict a dictionary holding JSON objects, to be imported in the model. + * @param err an error or NULL + */ + -(instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError**)err; + + +/** + * All JSONModel classes should implement initWithData:error: + * + * For most classes the default initWithData: inherited from JSONModel itself + * should suffice, but developers have the option ot also overwrite it if needed. + * + * @param data representing a JSON response (usually fetched from web), to be imported in the model. + * @param error an error or NULL + */ +-(instancetype)initWithData:(NSData*)data error:(NSError**)error; + +/** + * All JSONModel classes should be able to export themselves as a dictionary of + * JSON compliant objects. + * + * For most classes the inherited from JSONModel default toDictionary implementation + * should suffice. + * + * @return NSDictionary dictionary of JSON compliant objects + * @exception JSONModelTypeNotAllowedException thrown when one of your model's custom class properties + * does not have matching transformer method in an JSONValueTransformer. + */ + -(NSDictionary*)toDictionary; + + /** + * Export a model class to a dictionary, including only given properties + * + * @param propertyNames the properties to export; if nil, all properties exported + * @return NSDictionary dictionary of JSON compliant objects + * @exception JSONModelTypeNotAllowedException thrown when one of your model's custom class properties + * does not have matching transformer method in an JSONValueTransformer. + */ + -(NSDictionary*)toDictionaryWithKeys:(NSArray*)propertyNames; +@end + +///////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - JSONModel interface +/** + * The JSONModel is an abstract model class, you should not instantiate it directly, + * as it does not have any properties, and therefore cannot serve as a data model. + * Instead you should subclass it, and define the properties you want your data model + * to have as properties of your own class. + */ +@interface JSONModel : NSObject + +/** @name Creating and initializing models */ + + /** + * Create a new model instance and initialize it with the JSON from a text parameter. The method assumes UTF8 encoded input text. + * @param string JSON text data + * @param err an initialization error or nil + * @exception JSONModelTypeNotAllowedException thrown when unsupported type is found in the incoming JSON, + * or a property type in your model is not supported by JSONValueTransformer and its categories + * @see initWithString:usingEncoding:error: for use of custom text encodings + */ + -(instancetype)initWithString:(NSString*)string error:(JSONModelError**)err; + + /** + * Create a new model instance and initialize it with the JSON from a text parameter using the given encoding. + * @param string JSON text data + * @param encoding the text encoding to use when parsing the string (see NSStringEncoding) + * @param err an initialization error or nil + * @exception JSONModelTypeNotAllowedException thrown when unsupported type is found in the incoming JSON, + * or a property type in your model is not supported by JSONValueTransformer and its categories + */ + -(instancetype)initWithString:(NSString *)string usingEncoding:(NSStringEncoding)encoding error:(JSONModelError**)err; + + -(instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err; + + -(instancetype)initWithData:(NSData *)data error:(NSError **)error; + +/** @name Exporting model contents */ + + /** + * Export the whole object to a dictionary + * @return dictionary containing the data model + */ + -(NSDictionary*)toDictionary; + + /** + * Export the whole object to a JSON data text string + * @return JSON text describing the data model + */ + -(NSString*)toJSONString; + + /** + * Export the whole object to a JSON data text string + * @return JSON text data describing the data model + */ + -(NSData*)toJSONData; + + /** + * Export the specified properties of the object to a dictionary + * @param propertyNames the properties to export; if nil, all properties exported + * @return dictionary containing the data model + */ + -(NSDictionary*)toDictionaryWithKeys:(NSArray*)propertyNames; + + /** + * Export the specified properties of the object to a JSON data text string + * @param propertyNames the properties to export; if nil, all properties exported + * @return JSON text describing the data model + */ + -(NSString*)toJSONStringWithKeys:(NSArray*)propertyNames; + + /** + * Export the specified properties of the object to a JSON data text string + * @param propertyNames the properties to export; if nil, all properties exported + * @return JSON text data describing the data model + */ + -(NSData*)toJSONDataWithKeys:(NSArray*)propertyNames; + +/** @name Batch methods */ + + /** + * If you have a list of dictionaries in a JSON feed, you can use this method to create an NSArray + * of model objects. Handy when importing JSON data lists. + * This method will loop over the input list and initialize a data model for every dictionary in the list. + * + * @param array list of dictionaries to be imported as models + * @return list of initialized data model objects + * @exception JSONModelTypeNotAllowedException thrown when unsupported type is found in the incoming JSON, + * or a property type in your model is not supported by JSONValueTransformer and its categories + * @exception JSONModelInvalidDataException thrown when the input data does not include all required keys + * @see arrayOfDictionariesFromModels: + */ + +(NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array __attribute__((deprecated("use arrayOfModelsFromDictionaries:error:"))); + +(NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array error:(NSError**)err; + +(NSMutableArray*)arrayOfModelsFromData:(NSData*)data error:(NSError**)err; + +(NSMutableArray*)arrayOfModelsFromString:(NSString*)string error:(NSError**)err; + +(NSMutableDictionary*)dictionaryOfModelsFromDictionary:(NSDictionary*)dictionary error:(NSError**)err; + +(NSMutableDictionary*)dictionaryOfModelsFromData:(NSData*)data error:(NSError**)err; + +(NSMutableDictionary*)dictionaryOfModelsFromString:(NSString*)string error:(NSError**)err; + + /** + * If you have an NSArray of data model objects, this method takes it in and outputs a list of the + * matching dictionaries. This method does the opposite of arrayOfObjectsFromDictionaries: + * @param array list of JSONModel objects + * @return a list of NSDictionary objects + * @exception JSONModelTypeNotAllowedException thrown when unsupported type is found in the incoming JSON, + * or a property type in your model is not supported by JSONValueTransformer and its categories + * @see arrayOfModelsFromDictionaries: + */ + +(NSMutableArray*)arrayOfDictionariesFromModels:(NSArray*)array; + +(NSMutableDictionary*)dictionaryOfDictionariesFromModels:(NSDictionary*)dictionary; + +/** @name Comparing models */ + + /** + * The name of the model's property, which is considered the model's unique identifier. + * You can define Index property by using the Index protocol: + * @property (strong, nonatomic) NSString<Index>* id; + */ + -(NSString*)indexPropertyName; + + /** + * Overridden NSObject method to compare model objects. Compares the <Index> property of the two models, + * if an index property is defined. + * @param object a JSONModel instance to compare to for equality + */ + -(BOOL)isEqual:(id)object; + + /** + * Comparison method, which uses the defined <Index> property of the two models, to compare them. + * If there isn't an index property throws an exception. If the Index property does not have a compare: method + * also throws an exception. NSString and NSNumber have compare: methods, and in case the Index property is + * a another custom class, the programmer should create a custom compare: method then. + * @param object a JSONModel instance to compare to + */ + -(NSComparisonResult)compare:(id)object; + +/** @name Validation */ + + /** + * Overwrite the validate method in your own models if you need to perform some custom validation over the model data. + * This method gets called at the very end of the JSONModel initializer, thus the model is in the state that you would + * get it back when initialized. Check the values of any property that needs to be validated and if any invalid values + * are encountered return NO and set the error parameter to an NSError object. If the model is valid return YES. + * + * NB: Only setting the error parameter is not enough to fail the validation, you also need to return a NO value. + * + * @param error a pointer to an NSError object, to pass back an error if needed + * @return a BOOL result, showing whether the model data validates or not. You can use the convenience method + * [JSONModelError errorModelIsInvalid] to set the NSError param if the data fails your custom validation + */ +-(BOOL)validate:(NSError**)error; + +/** @name Key mapping */ + /** + * Overwrite in your models if your property names don't match your JSON key names. + * Lookup JSONKeyMapper docs for more details. + */ ++(JSONKeyMapper*)keyMapper; + +/** + * Sets a key mapper which affects ALL the models in your project. Use this if you need only one mapper to work + * with your API. For example if you are using the [JSONKeyMapper mapperFromUnderscoreCaseToCamelCase] it is more + * likely that you will need to use it with ALL of your models. + * NB: Custom key mappers take precedence over the global key mapper. + * @param globalKeyMapper a key mapper to apply to all models in your project. + * + * Lookup JSONKeyMapper docs for more details. + */ ++(void)setGlobalKeyMapper:(JSONKeyMapper*)globalKeyMapper; + +/** + * Indicates whether the property with the given name is Optional. + * To have a model with all of its properties being Optional just return YES. + * This method returns by default NO, since the default behaviour is to have all properties required. + * @param propertyName the name of the property + * @return a BOOL result indicating whether the property is optional + */ ++(BOOL)propertyIsOptional:(NSString*)propertyName; + +/** + * Indicates whether the property with the given name is Ignored. + * To have a model with all of its properties being Ignored just return YES. + * This method returns by default NO, since the default behaviour is to have all properties required. + * @param propertyName the name of the property + * @return a BOOL result indicating whether the property is ignored + */ ++(BOOL)propertyIsIgnored:(NSString*)propertyName; + +/** + * Indicates the protocol name for an array property. + * Rather than using: + * @property (strong) NSArray* things; + * You can implement protocolForArrayProperty: and keep your property + * defined like: + * @property (strong) NSArray* things; + * @param propertyName the name of the property + * @return an NSString result indicating the name of the protocol/class + * that should be contained in this array property. Return nil to indicate + * no contained protocol. + */ ++(NSString*)protocolForArrayProperty:(NSString *)propertyName; + +/** + * Merges values from the given dictionary into the model instance. + * @param dict dictionary with values + * @param useKeyMapping if YES the method will use the model's key mapper and the global key mapper, if NO + * it'll just try to match the dictionary keys to the model's properties + */ +- (void)mergeFromDictionary:(NSDictionary *)dict useKeyMapping:(BOOL)useKeyMapping __attribute__((deprecated("use mergeFromDictionary:useKeyMapping:error:"))); +- (void)mergeFromDictionary:(NSDictionary *)dict useKeyMapping:(BOOL)useKeyMapping error:(NSError **)error; + +@end diff --git a/JSONModel/JSONModel/JSONModel.m b/JSONModel/JSONModel/JSONModel.m new file mode 100644 index 0000000..da7dd9e --- /dev/null +++ b/JSONModel/JSONModel/JSONModel.m @@ -0,0 +1,1396 @@ +// +// JSONModel.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#if !__has_feature(objc_arc) +#error The JSONMOdel framework is ARC only, you can enable ARC on per file basis. +#endif + + +#import +#import + + +#import "JSONModel.h" +#import "JSONModelClassProperty.h" +#import "JSONModelArray.h" + +#pragma mark - associated objects names +static const char * kMapperObjectKey; +static const char * kClassPropertiesKey; +static const char * kClassRequiredPropertyNamesKey; +static const char * kIndexPropertyNameKey; + +#pragma mark - class static variables +static NSArray* allowedJSONTypes = nil; +static NSArray* allowedPrimitiveTypes = nil; +static JSONValueTransformer* valueTransformer = nil; +static Class JSONModelClass = NULL; + +#pragma mark - model cache +static JSONKeyMapper* globalKeyMapper = nil; + +#pragma mark - JSONModel implementation +@implementation JSONModel +{ + NSString* _description; +} + +#pragma mark - initialization methods + ++(void)load +{ + static dispatch_once_t once; + dispatch_once(&once, ^{ + // initialize all class static objects, + // which are common for ALL JSONModel subclasses + + @autoreleasepool { + allowedJSONTypes = @[ + [NSString class], [NSNumber class], [NSDecimalNumber class], [NSArray class], [NSDictionary class], [NSNull class], //immutable JSON classes + [NSMutableString class], [NSMutableArray class], [NSMutableDictionary class] //mutable JSON classes + ]; + + allowedPrimitiveTypes = @[ + @"BOOL", @"float", @"int", @"long", @"double", @"short", + //and some famous aliases + @"NSInteger", @"NSUInteger", + @"Block" + ]; + + valueTransformer = [[JSONValueTransformer alloc] init]; + + // This is quite strange, but I found the test isSubclassOfClass: (line ~291) to fail if using [JSONModel class]. + // somewhat related: https://stackoverflow.com/questions/6524165/nsclassfromstring-vs-classnamednsstring + // //; seems to break the unit tests + + // Using NSClassFromString instead of [JSONModel class], as this was breaking unit tests, see below + //http://stackoverflow.com/questions/21394919/xcode-5-unit-test-seeing-wrong-class + JSONModelClass = NSClassFromString(NSStringFromClass(self)); + } + }); +} + +-(void)__setup__ +{ + //if first instance of this model, generate the property list + if (!objc_getAssociatedObject(self.class, &kClassPropertiesKey)) { + [self __inspectProperties]; + } + + //if there's a custom key mapper, store it in the associated object + id mapper = [[self class] keyMapper]; + if ( mapper && !objc_getAssociatedObject(self.class, &kMapperObjectKey) ) { + objc_setAssociatedObject( + self.class, + &kMapperObjectKey, + mapper, + OBJC_ASSOCIATION_RETAIN // This is atomic + ); + } +} + +-(id)init +{ + self = [super init]; + if (self) { + //do initial class setup + [self __setup__]; + } + return self; +} + +-(instancetype)initWithData:(NSData *)data error:(NSError *__autoreleasing *)err +{ + //check for nil input + if (!data) { + if (err) *err = [JSONModelError errorInputIsNil]; + return nil; + } + //read the json + JSONModelError* initError = nil; + id obj = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&initError]; + + if (initError) { + if (err) *err = [JSONModelError errorBadJSON]; + return nil; + } + + //init with dictionary + id objModel = [self initWithDictionary:obj error:&initError]; + if (initError && err) *err = initError; + return objModel; +} + +-(id)initWithString:(NSString*)string error:(JSONModelError**)err +{ + JSONModelError* initError = nil; + id objModel = [self initWithString:string usingEncoding:NSUTF8StringEncoding error:&initError]; + if (initError && err) *err = initError; + return objModel; +} + +-(id)initWithString:(NSString *)string usingEncoding:(NSStringEncoding)encoding error:(JSONModelError**)err +{ + //check for nil input + if (!string) { + if (err) *err = [JSONModelError errorInputIsNil]; + return nil; + } + + JSONModelError* initError = nil; + id objModel = [self initWithData:[string dataUsingEncoding:encoding] error:&initError]; + if (initError && err) *err = initError; + return objModel; + +} + +-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err +{ + //check for nil input + if (!dict) { + if (err) *err = [JSONModelError errorInputIsNil]; + return nil; + } + + //invalid input, just create empty instance + if (![dict isKindOfClass:[NSDictionary class]]) { + if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."]; + return nil; + } + + //create a class instance + self = [self init]; + if (!self) { + + //super init didn't succeed + if (err) *err = [JSONModelError errorModelIsInvalid]; + return nil; + } + + //check incoming data structure + if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) { + return nil; + } + + //import the data from a dictionary + if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) { + return nil; + } + + //run any custom model validation + if (![self validate:err]) { + return nil; + } + + //model is valid! yay! + return self; +} + +-(JSONKeyMapper*)__keyMapper +{ + //get the model key mapper + return objc_getAssociatedObject(self.class, &kMapperObjectKey); +} + +-(BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMapper:(JSONKeyMapper*)keyMapper error:(NSError**)err +{ + //check if all required properties are present + NSArray* incomingKeysArray = [dict allKeys]; + NSMutableSet* requiredProperties = [self __requiredPropertyNames].mutableCopy; + NSSet* incomingKeys = [NSSet setWithArray: incomingKeysArray]; + + //transform the key names, if necessary + if (keyMapper || globalKeyMapper) { + + NSMutableSet* transformedIncomingKeys = [NSMutableSet setWithCapacity: requiredProperties.count]; + NSString* transformedName = nil; + + //loop over the required properties list + for (JSONModelClassProperty* property in [self __properties__]) { + + transformedName = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name; + + //check if exists and if so, add to incoming keys + id value; + @try { + value = [dict valueForKeyPath:transformedName]; + } + @catch (NSException *exception) { + value = dict[transformedName]; + } + + if (value) { + [transformedIncomingKeys addObject: property.name]; + } + } + + //overwrite the raw incoming list with the mapped key names + incomingKeys = transformedIncomingKeys; + } + + //check for missing input keys + if (![requiredProperties isSubsetOfSet:incomingKeys]) { + + //get a list of the missing properties + [requiredProperties minusSet:incomingKeys]; + + //not all required properties are in - invalid input + JMLog(@"Incoming data was invalid [%@ initWithDictionary:]. Keys missing: %@", self.class, requiredProperties); + + if (err) *err = [JSONModelError errorInvalidDataWithMissingKeys:requiredProperties]; + return NO; + } + + //not needed anymore + incomingKeys= nil; + requiredProperties= nil; + + return YES; +} + +-(NSString*)__mapString:(NSString*)string withKeyMapper:(JSONKeyMapper*)keyMapper importing:(BOOL)importing +{ + if (keyMapper) { + //custom mapper + NSString* mappedName = [keyMapper convertValue:string isImportingToModel:importing]; + if (globalKeyMapper && [mappedName isEqualToString: string]) { + mappedName = [globalKeyMapper convertValue:string isImportingToModel:importing]; + } + string = mappedName; + } else if (globalKeyMapper) { + //global keymapper + string = [globalKeyMapper convertValue:string isImportingToModel:importing]; + } + + return string; +} + +-(BOOL)__importDictionary:(NSDictionary*)dict withKeyMapper:(JSONKeyMapper*)keyMapper validation:(BOOL)validation error:(NSError**)err +{ + //loop over the incoming keys and set self's properties + for (JSONModelClassProperty* property in [self __properties__]) { + + //convert key name ot model keys, if a mapper is provided + NSString* jsonKeyPath = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name; + //JMLog(@"keyPath: %@", jsonKeyPath); + + //general check for data type compliance + id jsonValue; + @try { + jsonValue = [dict valueForKeyPath: jsonKeyPath]; + } + @catch (NSException *exception) { + jsonValue = dict[jsonKeyPath]; + } + + //check for Optional properties + if (isNull(jsonValue)) { + //skip this property, continue with next property + if (property.isOptional || !validation) continue; + + if (err) { + //null value for required property + NSString* msg = [NSString stringWithFormat:@"Value of required model key %@ is null", property.name]; + JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg]; + *err = [dataErr errorByPrependingKeyPathComponent:property.name]; + } + return NO; + } + + Class jsonValueClass = [jsonValue class]; + BOOL isValueOfAllowedType = NO; + + for (Class allowedType in allowedJSONTypes) { + if ( [jsonValueClass isSubclassOfClass: allowedType] ) { + isValueOfAllowedType = YES; + break; + } + } + + if (isValueOfAllowedType==NO) { + //type not allowed + JMLog(@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass)); + + if (err) { + NSString* msg = [NSString stringWithFormat:@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass)]; + JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg]; + *err = [dataErr errorByPrependingKeyPathComponent:property.name]; + } + return NO; + } + + //check if there's matching property in the model + if (property) { + + // check for custom setter, than the model doesn't need to do any guessing + // how to read the property's value from JSON + if ([self __customSetValue:jsonValue forProperty:property]) { + //skip to next JSON key + continue; + }; + + // 0) handle primitives + if (property.type == nil && property.structName==nil) { + + //generic setter + if (jsonValue != [self valueForKey:property.name]) { + [self setValue:jsonValue forKey: property.name]; + } + + //skip directly to the next key + continue; + } + + // 0.5) handle nils + if (isNull(jsonValue)) { + if ([self valueForKey:property.name] != nil) { + [self setValue:nil forKey: property.name]; + } + continue; + } + + + // 1) check if property is itself a JSONModel + if ([self __isJSONModelSubClass:property.type]) { + + //initialize the property's model, store it + JSONModelError* initErr = nil; + id value = [[property.type alloc] initWithDictionary: jsonValue error:&initErr]; + + if (!value) { + //skip this property, continue with next property + if (property.isOptional || !validation) continue; + + // Propagate the error, including the property name as the key-path component + if((err != nil) && (initErr != nil)) + { + *err = [initErr errorByPrependingKeyPathComponent:property.name]; + } + return NO; + } + if (![value isEqual:[self valueForKey:property.name]]) { + [self setValue:value forKey: property.name]; + } + + //for clarity, does the same without continue + continue; + + } else { + + // 2) check if there's a protocol to the property + // ) might or not be the case there's a built in transform for it + if (property.protocol) { + + //JMLog(@"proto: %@", p.protocol); + jsonValue = [self __transform:jsonValue forProperty:property error:err]; + if (!jsonValue) { + if ((err != nil) && (*err == nil)) { + NSString* msg = [NSString stringWithFormat:@"Failed to transform value, but no error was set during transformation. (%@)", property]; + JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg]; + *err = [dataErr errorByPrependingKeyPathComponent:property.name]; + } + return NO; + } + } + + // 3.1) handle matching standard JSON types + if (property.isStandardJSONType && [jsonValue isKindOfClass: property.type]) { + + //mutable properties + if (property.isMutable) { + jsonValue = [jsonValue mutableCopy]; + } + + //set the property value + if (![jsonValue isEqual:[self valueForKey:property.name]]) { + [self setValue:jsonValue forKey: property.name]; + } + continue; + } + + // 3.3) handle values to transform + if ( + (![jsonValue isKindOfClass:property.type] && !isNull(jsonValue)) + || + //the property is mutable + property.isMutable + || + //custom struct property + property.structName + ) { + + // searched around the web how to do this better + // but did not find any solution, maybe that's the best idea? (hardly) + Class sourceClass = [JSONValueTransformer classByResolvingClusterClasses:[jsonValue class]]; + + //JMLog(@"to type: [%@] from type: [%@] transformer: [%@]", p.type, sourceClass, selectorName); + + //build a method selector for the property and json object classes + NSString* selectorName = [NSString stringWithFormat:@"%@From%@:", + (property.structName? property.structName : property.type), //target name + sourceClass]; //source name + SEL selector = NSSelectorFromString(selectorName); + + //check for custom transformer + BOOL foundCustomTransformer = NO; + if ([valueTransformer respondsToSelector:selector]) { + foundCustomTransformer = YES; + } else { + //try for hidden custom transformer + selectorName = [NSString stringWithFormat:@"__%@",selectorName]; + selector = NSSelectorFromString(selectorName); + if ([valueTransformer respondsToSelector:selector]) { + foundCustomTransformer = YES; + } + } + + //check if there's a transformer with that name + if (foundCustomTransformer) { + + //it's OK, believe me... +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + //transform the value + jsonValue = [valueTransformer performSelector:selector withObject:jsonValue]; +#pragma clang diagnostic pop + + if (![jsonValue isEqual:[self valueForKey:property.name]]) { + [self setValue:jsonValue forKey: property.name]; + } + + } else { + + // it's not a JSON data type, and there's no transformer for it + // if property type is not supported - that's a programmer mistake -> exception + @throw [NSException exceptionWithName:@"Type not allowed" + reason:[NSString stringWithFormat:@"%@ type not supported for %@.%@", property.type, [self class], property.name] + userInfo:nil]; + return NO; + } + + } else { + // 3.4) handle "all other" cases (if any) + if (![jsonValue isEqual:[self valueForKey:property.name]]) { + [self setValue:jsonValue forKey: property.name]; + } + } + } + } + } + + return YES; +} + +#pragma mark - property inspection methods + +-(BOOL)__isJSONModelSubClass:(Class)class +{ +// http://stackoverflow.com/questions/19883472/objc-nsobject-issubclassofclass-gives-incorrect-failure +#ifdef UNIT_TESTING + return [@"JSONModel" isEqualToString: NSStringFromClass([class superclass])]; +#else + return [class isSubclassOfClass:JSONModelClass]; +#endif +} + +//returns a set of the required keys for the model +-(NSMutableSet*)__requiredPropertyNames +{ + //fetch the associated property names + NSMutableSet* classRequiredPropertyNames = objc_getAssociatedObject(self.class, &kClassRequiredPropertyNamesKey); + + if (!classRequiredPropertyNames) { + classRequiredPropertyNames = [NSMutableSet set]; + [[self __properties__] enumerateObjectsUsingBlock:^(JSONModelClassProperty* p, NSUInteger idx, BOOL *stop) { + if (!p.isOptional) [classRequiredPropertyNames addObject:p.name]; + }]; + + //persist the list + objc_setAssociatedObject( + self.class, + &kClassRequiredPropertyNamesKey, + classRequiredPropertyNames, + OBJC_ASSOCIATION_RETAIN // This is atomic + ); + } + return classRequiredPropertyNames; +} + +//returns a list of the model's properties +-(NSArray*)__properties__ +{ + //fetch the associated object + NSDictionary* classProperties = objc_getAssociatedObject(self.class, &kClassPropertiesKey); + if (classProperties) return [classProperties allValues]; + + //if here, the class needs to inspect itself + [self __setup__]; + + //return the property list + classProperties = objc_getAssociatedObject(self.class, &kClassPropertiesKey); + return [classProperties allValues]; +} + +//inspects the class, get's a list of the class properties +-(void)__inspectProperties +{ + //JMLog(@"Inspect class: %@", [self class]); + + NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary]; + + //temp variables for the loops + Class class = [self class]; + NSScanner* scanner = nil; + NSString* propertyType = nil; + + // inspect inherited properties up to the JSONModel class + while (class != [JSONModel class]) { + //JMLog(@"inspecting: %@", NSStringFromClass(class)); + + unsigned int propertyCount; + objc_property_t *properties = class_copyPropertyList(class, &propertyCount); + + //loop over the class properties + for (unsigned int i = 0; i < propertyCount; i++) { + + JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init]; + + //get property name + objc_property_t property = properties[i]; + const char *propertyName = property_getName(property); + p.name = @(propertyName); + + //JMLog(@"property: %@", p.name); + + //get property attributes + const char *attrs = property_getAttributes(property); + NSString* propertyAttributes = @(attrs); + NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","]; + + //ignore read-only properties + if ([attributeItems containsObject:@"R"]) { + continue; //to next property + } + + //check for 64b BOOLs + if ([propertyAttributes hasPrefix:@"Tc,"]) { + //mask BOOLs as structs so they can have custom converters + p.structName = @"BOOL"; + } + + scanner = [NSScanner scannerWithString: propertyAttributes]; + + //JMLog(@"attr: %@", [NSString stringWithCString:attrs encoding:NSUTF8StringEncoding]); + [scanner scanUpToString:@"T" intoString: nil]; + [scanner scanString:@"T" intoString:nil]; + + //check if the property is an instance of a class + if ([scanner scanString:@"@\"" intoString: &propertyType]) { + + [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"] + intoString:&propertyType]; + + //JMLog(@"type: %@", propertyClassName); + p.type = NSClassFromString(propertyType); + p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound); + p.isStandardJSONType = [allowedJSONTypes containsObject:p.type]; + + //read through the property protocols + while ([scanner scanString:@"<" intoString:NULL]) { + + NSString* protocolName = nil; + + [scanner scanUpToString:@">" intoString: &protocolName]; + + if ([protocolName isEqualToString:@"Optional"]) { + p.isOptional = YES; + } else if([protocolName isEqualToString:@"Index"]) { + p.isIndex = YES; + objc_setAssociatedObject( + self.class, + &kIndexPropertyNameKey, + p.name, + OBJC_ASSOCIATION_RETAIN // This is atomic + ); + } else if([protocolName isEqualToString:@"ConvertOnDemand"]) { + p.convertsOnDemand = YES; + } else if([protocolName isEqualToString:@"Ignore"]) { + p = nil; + } else { + p.protocol = protocolName; + } + + [scanner scanString:@">" intoString:NULL]; + } + + } + //check if the property is a structure + else if ([scanner scanString:@"{" intoString: &propertyType]) { + [scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet] + intoString:&propertyType]; + + p.isStandardJSONType = NO; + p.structName = propertyType; + + } + //the property must be a primitive + else { + + //the property contains a primitive data type + [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","] + intoString:&propertyType]; + + //get the full name of the primitive type + propertyType = valueTransformer.primitivesNames[propertyType]; + + if (![allowedPrimitiveTypes containsObject:propertyType]) { + + //type not allowed - programmer mistaken -> exception + @throw [NSException exceptionWithName:@"JSONModelProperty type not allowed" + reason:[NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", self.class, p.name] + userInfo:nil]; + } + + } + + NSString *nsPropertyName = @(propertyName); + if([[self class] propertyIsOptional:nsPropertyName]){ + p.isOptional = YES; + } + + if([[self class] propertyIsIgnored:nsPropertyName]){ + p = nil; + } + + NSString* customProtocol = [[self class] protocolForArrayProperty:nsPropertyName]; + if (customProtocol) { + p.protocol = customProtocol; + } + + //few cases where JSONModel will ignore properties automatically + if ([propertyType isEqualToString:@"Block"]) { + p = nil; + } + + //add the property object to the temp index + if (p && ![propertyIndex objectForKey:p.name]) { + [propertyIndex setValue:p forKey:p.name]; + } + } + + free(properties); + + //ascend to the super of the class + //(will do that until it reaches the root class - JSONModel) + class = [class superclass]; + } + + //finally store the property index in the static property index + objc_setAssociatedObject( + self.class, + &kClassPropertiesKey, + [propertyIndex copy], + OBJC_ASSOCIATION_RETAIN // This is atomic + ); +} + +#pragma mark - built-in transformer methods +//few built-in transformations +-(id)__transform:(id)value forProperty:(JSONModelClassProperty*)property error:(NSError**)err +{ + Class protocolClass = NSClassFromString(property.protocol); + if (!protocolClass) { + + //no other protocols on arrays and dictionaries + //except JSONModel classes + if ([value isKindOfClass:[NSArray class]]) { + @throw [NSException exceptionWithName:@"Bad property protocol declaration" + reason:[NSString stringWithFormat:@"<%@> is not allowed JSONModel property protocol, and not a JSONModel class.", property.protocol] + userInfo:nil]; + } + return value; + } + + //if the protocol is actually a JSONModel class + if ([self __isJSONModelSubClass:protocolClass]) { + + //check if it's a list of models + if ([property.type isSubclassOfClass:[NSArray class]]) { + + // Expecting an array, make sure 'value' is an array + if(![[value class] isSubclassOfClass:[NSArray class]]) + { + if(err != nil) + { + NSString* mismatch = [NSString stringWithFormat:@"Property '%@' is declared as NSArray<%@>* but the corresponding JSON value is not a JSON Array.", property.name, property.protocol]; + JSONModelError* typeErr = [JSONModelError errorInvalidDataWithTypeMismatch:mismatch]; + *err = [typeErr errorByPrependingKeyPathComponent:property.name]; + } + return nil; + } + + if (property.convertsOnDemand) { + //on demand conversion + value = [[JSONModelArray alloc] initWithArray:value modelClass:[protocolClass class]]; + + } else { + //one shot conversion + JSONModelError* arrayErr = nil; + value = [[protocolClass class] arrayOfModelsFromDictionaries:value error:&arrayErr]; + if((err != nil) && (arrayErr != nil)) + { + *err = [arrayErr errorByPrependingKeyPathComponent:property.name]; + return nil; + } + } + } + + //check if it's a dictionary of models + if ([property.type isSubclassOfClass:[NSDictionary class]]) { + + // Expecting a dictionary, make sure 'value' is a dictionary + if(![[value class] isSubclassOfClass:[NSDictionary class]]) + { + if(err != nil) + { + NSString* mismatch = [NSString stringWithFormat:@"Property '%@' is declared as NSDictionary<%@>* but the corresponding JSON value is not a JSON Object.", property.name, property.protocol]; + JSONModelError* typeErr = [JSONModelError errorInvalidDataWithTypeMismatch:mismatch]; + *err = [typeErr errorByPrependingKeyPathComponent:property.name]; + } + return nil; + } + + NSMutableDictionary* res = [NSMutableDictionary dictionary]; + + for (NSString* key in [value allKeys]) { + JSONModelError* initErr = nil; + id obj = [[[protocolClass class] alloc] initWithDictionary:value[key] error:&initErr]; + if (obj == nil) + { + // Propagate the error, including the property name as the key-path component + if((err != nil) && (initErr != nil)) + { + initErr = [initErr errorByPrependingKeyPathComponent:key]; + *err = [initErr errorByPrependingKeyPathComponent:property.name]; + } + return nil; + } + [res setValue:obj forKey:key]; + } + value = [NSDictionary dictionaryWithDictionary:res]; + } + } + + return value; +} + +//built-in reverse transformations (export to JSON compliant objects) +-(id)__reverseTransform:(id)value forProperty:(JSONModelClassProperty*)property +{ + Class protocolClass = NSClassFromString(property.protocol); + if (!protocolClass) return value; + + //if the protocol is actually a JSONModel class + if ([self __isJSONModelSubClass:protocolClass]) { + + //check if should export list of dictionaries + if (property.type == [NSArray class] || property.type == [NSMutableArray class]) { + NSMutableArray* tempArray = [NSMutableArray arrayWithCapacity: [(NSArray*)value count] ]; + for (NSObject* model in (NSArray*)value) { + if ([model respondsToSelector:@selector(toDictionary)]) { + [tempArray addObject: [model toDictionary]]; + } else + [tempArray addObject: model]; + } + return [tempArray copy]; + } + + //check if should export dictionary of dictionaries + if (property.type == [NSDictionary class] || property.type == [NSMutableDictionary class]) { + NSMutableDictionary* res = [NSMutableDictionary dictionary]; + for (NSString* key in [(NSDictionary*)value allKeys]) { + id model = value[key]; + [res setValue: [model toDictionary] forKey: key]; + } + return [NSDictionary dictionaryWithDictionary:res]; + } + } + + return value; +} + +#pragma mark - custom transformations +-(BOOL)__customSetValue:(id)value forProperty:(JSONModelClassProperty*)property +{ + if (!property.customSetters) + property.customSetters = [NSMutableDictionary new]; + + NSString *className = NSStringFromClass([JSONValueTransformer classByResolvingClusterClasses:[value class]]); + + if (!property.customSetters[className]) { + //check for a custom property setter method + NSString* ucfirstName = [property.name stringByReplacingCharactersInRange:NSMakeRange(0,1) + withString:[[property.name substringToIndex:1] uppercaseString]]; + NSString* selectorName = [NSString stringWithFormat:@"set%@With%@:", ucfirstName, className]; + + SEL customPropertySetter = NSSelectorFromString(selectorName); + + //check if there's a custom selector like this + if (![self respondsToSelector: customPropertySetter]) { + property.customSetters[className] = [NSNull null]; + return NO; + } + + //cache the custom setter selector + property.customSetters[className] = selectorName; + } + + if (property.customSetters[className] != [NSNull null]) { + //call the custom setter + //https://github.com/steipete + SEL selector = NSSelectorFromString(property.customSetters[className]); + ((void (*) (id, SEL, id))objc_msgSend)(self, selector, value); + return YES; + } + + return NO; +} + +-(BOOL)__customGetValue:(id*)value forProperty:(JSONModelClassProperty*)property +{ + if (property.getterType == kNotInspected) { + //check for a custom property getter method + NSString* ucfirstName = [property.name stringByReplacingCharactersInRange: NSMakeRange(0,1) + withString: [[property.name substringToIndex:1] uppercaseString]]; + NSString* selectorName = [NSString stringWithFormat:@"JSONObjectFor%@", ucfirstName]; + + SEL customPropertyGetter = NSSelectorFromString(selectorName); + if (![self respondsToSelector: customPropertyGetter]) { + property.getterType = kNo; + return NO; + } + + property.getterType = kCustom; + property.customGetter = customPropertyGetter; + + } + + if (property.getterType==kCustom) { + //call the custom getter + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + *value = [self performSelector:property.customGetter]; + #pragma clang diagnostic pop + return YES; + } + + return NO; +} + +#pragma mark - persistance +-(void)__createDictionariesForKeyPath:(NSString*)keyPath inDictionary:(NSMutableDictionary**)dict +{ + //find if there's a dot left in the keyPath + NSUInteger dotLocation = [keyPath rangeOfString:@"."].location; + if (dotLocation==NSNotFound) return; + + //inspect next level + NSString* nextHierarchyLevelKeyName = [keyPath substringToIndex: dotLocation]; + NSDictionary* nextLevelDictionary = (*dict)[nextHierarchyLevelKeyName]; + + if (nextLevelDictionary==nil) { + //create non-existing next level here + nextLevelDictionary = [NSMutableDictionary dictionary]; + } + + //recurse levels + [self __createDictionariesForKeyPath:[keyPath substringFromIndex: dotLocation+1] + inDictionary:&nextLevelDictionary ]; + + //create the hierarchy level + [*dict setValue:nextLevelDictionary forKeyPath: nextHierarchyLevelKeyName]; +} + +-(NSDictionary*)toDictionary +{ + return [self toDictionaryWithKeys:nil]; +} + +-(NSString*)toJSONString +{ + return [self toJSONStringWithKeys:nil]; +} + +-(NSData*)toJSONData +{ + return [self toJSONDataWithKeys:nil]; +} + +//exports the model as a dictionary of JSON compliant objects +-(NSDictionary*)toDictionaryWithKeys:(NSArray*)propertyNames +{ + NSArray* properties = [self __properties__]; + NSMutableDictionary* tempDictionary = [NSMutableDictionary dictionaryWithCapacity:properties.count]; + + id value; + + //loop over all properties + for (JSONModelClassProperty* p in properties) { + + //skip if unwanted + if (propertyNames != nil && ![propertyNames containsObject:p.name]) + continue; + + //fetch key and value + NSString* keyPath = (self.__keyMapper||globalKeyMapper) ? [self __mapString:p.name withKeyMapper:self.__keyMapper importing:YES] : p.name; + value = [self valueForKey: p.name]; + + //JMLog(@"toDictionary[%@]->[%@] = '%@'", p.name, keyPath, value); + + if ([keyPath rangeOfString:@"."].location != NSNotFound) { + //there are sub-keys, introduce dictionaries for them + [self __createDictionariesForKeyPath:keyPath inDictionary:&tempDictionary]; + } + + //check for custom getter + if ([self __customGetValue:&value forProperty:p]) { + //custom getter, all done + [tempDictionary setValue:value forKeyPath:keyPath]; + continue; + } + + //export nil when they are not optional values as JSON null, so that the structure of the exported data + //is still valid if it's to be imported as a model again + if (isNull(value)) { + + if (p.isOptional) + { + [tempDictionary removeObjectForKey:keyPath]; + } + else + { + [tempDictionary setValue:[NSNull null] forKeyPath:keyPath]; + } + continue; + } + + //check if the property is another model + if ([value isKindOfClass:JSONModelClass]) { + + //recurse models + value = [(JSONModel*)value toDictionary]; + [tempDictionary setValue:value forKeyPath: keyPath]; + + //for clarity + continue; + + } else { + + // 1) check for built-in transformation + if (p.protocol) { + value = [self __reverseTransform:value forProperty:p]; + } + + // 2) check for standard types OR 2.1) primitives + if (p.structName==nil && (p.isStandardJSONType || p.type==nil)) { + + //generic get value + [tempDictionary setValue:value forKeyPath: keyPath]; + + continue; + } + + // 3) try to apply a value transformer + if (YES) { + + //create selector from the property's class name + NSString* selectorName = [NSString stringWithFormat:@"%@From%@:", @"JSONObject", p.type?p.type:p.structName]; + SEL selector = NSSelectorFromString(selectorName); + + BOOL foundCustomTransformer = NO; + if ([valueTransformer respondsToSelector:selector]) { + foundCustomTransformer = YES; + } else { + //try for hidden transformer + selectorName = [NSString stringWithFormat:@"__%@",selectorName]; + selector = NSSelectorFromString(selectorName); + if ([valueTransformer respondsToSelector:selector]) { + foundCustomTransformer = YES; + } + } + + //check if there's a transformer declared + if (foundCustomTransformer) { + + //it's OK, believe me... +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + value = [valueTransformer performSelector:selector withObject:value]; +#pragma clang diagnostic pop + + [tempDictionary setValue:value forKeyPath: keyPath]; + + } else { + + //in this case most probably a custom property was defined in a model + //but no default reverse transformer for it + @throw [NSException exceptionWithName:@"Value transformer not found" + reason:[NSString stringWithFormat:@"[JSONValueTransformer %@] not found", selectorName] + userInfo:nil]; + return nil; + } + } + } + } + + return [tempDictionary copy]; +} + +//exports model to a dictionary and then to a JSON string +-(NSData*)toJSONDataWithKeys:(NSArray*)propertyNames +{ + NSData* jsonData = nil; + NSError* jsonError = nil; + + @try { + NSDictionary* dict = [self toDictionaryWithKeys:propertyNames]; + jsonData = [NSJSONSerialization dataWithJSONObject:dict options:kNilOptions error:&jsonError]; + } + @catch (NSException *exception) { + //this should not happen in properly design JSONModel + //usually means there was no reverse transformer for a custom property + JMLog(@"EXCEPTION: %@", exception.description); + return nil; + } + + return jsonData; +} + +-(NSString*)toJSONStringWithKeys:(NSArray*)propertyNames +{ + return [[NSString alloc] initWithData: [self toJSONDataWithKeys: propertyNames] + encoding: NSUTF8StringEncoding]; +} + +#pragma mark - import/export of lists +//loop over an NSArray of JSON objects and turn them into models ++(NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array +{ + return [self arrayOfModelsFromDictionaries:array error:nil]; +} + ++ (NSMutableArray *)arrayOfModelsFromData:(NSData *)data error:(NSError **)err +{ + id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:err]; + if (!json || ![json isKindOfClass:[NSArray class]]) return nil; + + return [self arrayOfModelsFromDictionaries:json error:err]; +} + ++ (NSMutableArray *)arrayOfModelsFromString:(NSString *)string error:(NSError **)err +{ + return [self arrayOfModelsFromData:[string dataUsingEncoding:NSUTF8StringEncoding] error:err]; +} + +// Same as above, but with error reporting ++(NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array error:(NSError**)err +{ + //bail early + if (isNull(array)) return nil; + + //parse dictionaries to objects + NSMutableArray* list = [NSMutableArray arrayWithCapacity: [array count]]; + + for (id d in array) + { + if ([d isKindOfClass:NSDictionary.class]) + { + JSONModelError* initErr = nil; + id obj = [[self alloc] initWithDictionary:d error:&initErr]; + if (obj == nil) + { + // Propagate the error, including the array index as the key-path component + if((err != nil) && (initErr != nil)) + { + NSString* path = [NSString stringWithFormat:@"[%lu]", (unsigned long)list.count]; + *err = [initErr errorByPrependingKeyPathComponent:path]; + } + return nil; + } + + [list addObject: obj]; + } else if ([d isKindOfClass:NSArray.class]) + { + [list addObjectsFromArray:[self arrayOfModelsFromDictionaries:d error:err]]; + } else + { + // This is very bad + } + + } + + return list; +} + ++ (NSMutableDictionary *)dictionaryOfModelsFromString:(NSString *)string error:(NSError **)err +{ + return [self dictionaryOfModelsFromData:[string dataUsingEncoding:NSUTF8StringEncoding] error:err]; +} + ++ (NSMutableDictionary *)dictionaryOfModelsFromData:(NSData *)data error:(NSError **)err +{ + id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:err]; + if (!json || ![json isKindOfClass:[NSDictionary class]]) return nil; + + return [self dictionaryOfModelsFromDictionary:json error:err]; +} + ++ (NSMutableDictionary *)dictionaryOfModelsFromDictionary:(NSDictionary *)dictionary error:(NSError **)err +{ + NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:dictionary.count]; + + for (NSString *key in dictionary.allKeys) + { + id object = dictionary[key]; + + if ([object isKindOfClass:NSDictionary.class]) + { + id obj = [[self alloc] initWithDictionary:object error:err]; + if (obj == nil) return nil; + output[key] = obj; + } + else if ([object isKindOfClass:NSArray.class]) + { + id obj = [self arrayOfModelsFromDictionaries:object error:err]; + if (obj == nil) return nil; + output[key] = obj; + } + else + { + *err = [JSONModelError errorInvalidDataWithTypeMismatch:@"Only dictionaries and arrays are supported"]; + return nil; + } + } + + return output; +} + +//loop over NSArray of models and export them to JSON objects ++(NSMutableArray*)arrayOfDictionariesFromModels:(NSArray*)array +{ + //bail early + if (isNull(array)) return nil; + + //convert to dictionaries + NSMutableArray* list = [NSMutableArray arrayWithCapacity: [array count]]; + + for (id object in array) { + + id obj = [object toDictionary]; + if (!obj) return nil; + + [list addObject: obj]; + } + return list; +} + +//loop over NSArray of models and export them to JSON objects with specific properties ++(NSMutableArray*)arrayOfDictionariesFromModels:(NSArray*)array propertyNamesToExport:(NSArray*)propertyNamesToExport; +{ + //bail early + if (isNull(array)) return nil; + + //convert to dictionaries + NSMutableArray* list = [NSMutableArray arrayWithCapacity: [array count]]; + + for (id object in array) { + + id obj = [object toDictionaryWithKeys:propertyNamesToExport]; + if (!obj) return nil; + + [list addObject: obj]; + } + return list; +} + ++(NSMutableDictionary *)dictionaryOfDictionariesFromModels:(NSDictionary *)dictionary +{ + //bail early + if (isNull(dictionary)) return nil; + + NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:dictionary.count]; + + for (NSString *key in dictionary.allKeys) { + id object = dictionary[key]; + id obj = [object toDictionary]; + if (!obj) return nil; + output[key] = obj; + } + + return output; +} + +#pragma mark - custom comparison methods +-(NSString*)indexPropertyName +{ + //custom getter for an associated object + return objc_getAssociatedObject(self.class, &kIndexPropertyNameKey); +} + +-(BOOL)isEqual:(id)object +{ + //bail early if different classes + if (![object isMemberOfClass:[self class]]) return NO; + + if (self.indexPropertyName) { + //there's a defined ID property + id objectId = [object valueForKey: self.indexPropertyName]; + return [[self valueForKey: self.indexPropertyName] isEqual:objectId]; + } + + //default isEqual implementation + return [super isEqual:object]; +} + +-(NSComparisonResult)compare:(id)object +{ + if (self.indexPropertyName) { + id objectId = [object valueForKey: self.indexPropertyName]; + if ([objectId respondsToSelector:@selector(compare:)]) { + return [[self valueForKey:self.indexPropertyName] compare:objectId]; + } + } + + //on purpose postponing the asserts for speed optimization + //these should not happen anyway in production conditions + NSAssert(self.indexPropertyName, @"Can't compare models with no property"); + NSAssert1(NO, @"The property of %@ is not comparable class.", [self class]); + return kNilOptions; +} + +- (NSUInteger)hash +{ + if (self.indexPropertyName) { + id val = [self valueForKey:self.indexPropertyName]; + + if (val) { + return [val hash]; + } + } + + return [super hash]; +} + +#pragma mark - custom data validation +-(BOOL)validate:(NSError**)error +{ + return YES; +} + +#pragma mark - custom recursive description +//custom description method for debugging purposes +-(NSString*)description +{ + NSMutableString* text = [NSMutableString stringWithFormat:@"<%@> \n", [self class]]; + + for (JSONModelClassProperty *p in [self __properties__]) { + + id value = ([p.name isEqualToString:@"description"])?self->_description:[self valueForKey:p.name]; + NSString* valueDescription = (value)?[value description]:@""; + + if (p.isStandardJSONType && ![value respondsToSelector:@selector(count)] && [valueDescription length]>60 && !p.convertsOnDemand) { + + //cap description for longer values + valueDescription = [NSString stringWithFormat:@"%@...", [valueDescription substringToIndex:59]]; + } + valueDescription = [valueDescription stringByReplacingOccurrencesOfString:@"\n" withString:@"\n "]; + [text appendFormat:@" [%@]: %@\n", p.name, valueDescription]; + } + + [text appendFormat:@"", [self class]]; + return text; +} + +#pragma mark - key mapping ++(JSONKeyMapper*)keyMapper +{ + return nil; +} + ++(void)setGlobalKeyMapper:(JSONKeyMapper*)globalKeyMapperParam +{ + globalKeyMapper = globalKeyMapperParam; +} + ++(BOOL)propertyIsOptional:(NSString*)propertyName +{ + return NO; +} + ++(BOOL)propertyIsIgnored:(NSString *)propertyName +{ + return NO; +} + ++(NSString*)protocolForArrayProperty:(NSString *)propertyName +{ + return nil; +} + +#pragma mark - working with incomplete models +- (void)mergeFromDictionary:(NSDictionary *)dict useKeyMapping:(BOOL)useKeyMapping +{ + [self mergeFromDictionary:dict useKeyMapping:useKeyMapping error:nil]; +} + +- (void)mergeFromDictionary:(NSDictionary *)dict useKeyMapping:(BOOL)useKeyMapping error:(NSError **)error +{ + [self __importDictionary:dict withKeyMapper:(useKeyMapping)? self.__keyMapper:nil validation:NO error:error]; +} + +#pragma mark - NSCopying, NSCoding +-(instancetype)copyWithZone:(NSZone *)zone +{ + return [NSKeyedUnarchiver unarchiveObjectWithData: + [NSKeyedArchiver archivedDataWithRootObject:self] + ]; +} + +-(instancetype)initWithCoder:(NSCoder *)decoder +{ + NSString* json = [decoder decodeObjectForKey:@"json"]; + + JSONModelError *error = nil; + self = [self initWithString:json error:&error]; + if (error) { + JMLog(@"%@",[error localizedDescription]); + } + return self; +} + +-(void)encodeWithCoder:(NSCoder *)encoder +{ + [encoder encodeObject:self.toJSONString forKey:@"json"]; +} + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +@end diff --git a/JSONModel/JSONModel/JSONModelArray.h b/JSONModel/JSONModel/JSONModelArray.h new file mode 100644 index 0000000..21251b1 --- /dev/null +++ b/JSONModel/JSONModel/JSONModelArray.h @@ -0,0 +1,60 @@ +// +// JSONModelArray.h +// +// @version 0.8.0 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +/** + * **Don't make instances of JSONModelArray yourself, except you know what you are doing.** + * + * You get automatically JSONModelArray instances, when you declare a convert on demand property, like so: + * + * @property (strong, nonatomic) NSArray<JSONModel, ConvertOnDemand>* list; + * + * The class stores its contents as they come from JSON, and upon the first request + * of each of the objects stored in the array, it'll be converted to the target model class. + * Thus saving time upon the very first model creation. + */ +@interface JSONModelArray : NSObject + +/** + * Don't make instances of JSONModelArray yourself, except you know what you are doing. + * + * @param array an array of NSDictionary objects + * @param cls the JSONModel sub-class you'd like the NSDictionaries to be converted to on demand + */ +- (id)initWithArray:(NSArray *)array modelClass:(Class)cls; + +- (id)objectAtIndex:(NSUInteger)index; +- (id)objectAtIndexedSubscript:(NSUInteger)index; +- (void)forwardInvocation:(NSInvocation *)anInvocation; +- (NSUInteger)count; +- (id)firstObject; +- (id)lastObject; + +/** + * Looks up the array's contents and tries to find a JSONModel object + * with matching index property value to the indexValue param. + * + * Will return nil if no matching model is found. Will return nil if there's no index property + * defined on the models found in the array (will sample the first object, assuming the array + * contains homogeneous collection of objects) + * + * @param indexValue the id value to search for + * @return the found model or nil + */ +- (id)modelWithIndexValue:(id)indexValue; + +@end diff --git a/JSONModel/JSONModel/JSONModelArray.m b/JSONModel/JSONModel/JSONModelArray.m new file mode 100644 index 0000000..44c7fa1 --- /dev/null +++ b/JSONModel/JSONModel/JSONModelArray.m @@ -0,0 +1,145 @@ +// +// JSONModelArray.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModelArray.h" +#import "JSONModel.h" + +@implementation JSONModelArray +{ + NSMutableArray* _storage; + Class _targetClass; +} + +-(id)initWithArray:(NSArray *)array modelClass:(Class)cls +{ + self = [super init]; + + if (self) { + _storage = [NSMutableArray arrayWithArray:array]; + _targetClass = cls; + } + return self; +} + +-(id)firstObject +{ + return [self objectAtIndex:0]; +} + +-(id)lastObject +{ + return [self objectAtIndex:_storage.count - 1]; +} + +-(id)objectAtIndex:(NSUInteger)index +{ + return [self objectAtIndexedSubscript:index]; +} + +-(id)objectAtIndexedSubscript:(NSUInteger)index +{ + id object = _storage[index]; + if (![object isMemberOfClass:_targetClass]) { + NSError* err = nil; + object = [[_targetClass alloc] initWithDictionary:object error:&err]; + if (object) { + _storage[index] = object; + } + } + return object; +} + +-(void)forwardInvocation:(NSInvocation *)anInvocation +{ + [anInvocation invokeWithTarget:_storage]; +} + +-(id)forwardingTargetForSelector:(SEL)selector +{ + static NSArray *overriddenMethods = nil; + if (!overriddenMethods) overriddenMethods = @[@"initWithArray:modelClass:", @"objectAtIndex:", @"objectAtIndexedSubscript:", @"count", @"modelWithIndexValue:", @"description", @"mutableCopy", @"firstObject", @"lastObject", @"countByEnumeratingWithState:objects:count:"]; + if ([overriddenMethods containsObject:NSStringFromSelector(selector)]) { + return self; + } + return _storage; +} + +-(NSUInteger)count +{ + return _storage.count; +} + +-(id)modelWithIndexValue:(id)indexValue +{ + if (self.count==0) return nil; + if (![_storage[0] indexPropertyName]) return nil; + + for (JSONModel* model in _storage) { + if ([[model valueForKey:model.indexPropertyName] isEqual:indexValue]) { + return model; + } + } + + return nil; +} + +-(id)mutableCopy +{ + //it's already mutable + return self; +} + +#pragma mark - description +-(NSString*)description +{ + NSMutableString* res = [NSMutableString stringWithFormat:@"\n", [_targetClass description]]; + for (id m in _storage) { + [res appendString: [m description]]; + [res appendString: @",\n"]; + } + [res appendFormat:@"\n"]; + return res; +} + +-(NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])stackbuf + count:(NSUInteger)stackbufLength +{ + NSUInteger count = 0; + + unsigned long countOfItemsAlreadyEnumerated = state->state; + + if (countOfItemsAlreadyEnumerated == 0) { + state->mutationsPtr = &state->extra[0]; + } + + if (countOfItemsAlreadyEnumerated < [self count]) { + state->itemsPtr = stackbuf; + while ((countOfItemsAlreadyEnumerated < [self count]) && (count < stackbufLength)) { + stackbuf[count] = [self objectAtIndex:countOfItemsAlreadyEnumerated]; + countOfItemsAlreadyEnumerated++; + count++; + } + } else { + count = 0; + } + + state->state = countOfItemsAlreadyEnumerated; + + return count; +} + +@end diff --git a/JSONModel/JSONModel/JSONModelClassProperty.h b/JSONModel/JSONModel/JSONModelClassProperty.h new file mode 100644 index 0000000..8b0949d --- /dev/null +++ b/JSONModel/JSONModel/JSONModelClassProperty.h @@ -0,0 +1,73 @@ +// +// JSONModelClassProperty.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +enum kCustomizationTypes { + kNotInspected = 0, + kCustom, + kNo + }; + +typedef enum kCustomizationTypes PropertyGetterType; + +/** + * **You do not need to instantiate this class yourself.** This class is used internally by JSONModel + * to inspect the declared properties of your model class. + * + * Class to contain the information, representing a class property + * It features the property's name, type, whether it's a required property, + * and (optionally) the class protocol + */ +@interface JSONModelClassProperty : NSObject + +/** The name of the declared property (not the ivar name) */ +@property (copy, nonatomic) NSString* name; + +/** A property class type */ +@property (assign, nonatomic) Class type; + +/** Struct name if a struct */ +@property (strong, nonatomic) NSString* structName; + +/** The name of the protocol the property conforms to (or nil) */ +@property (copy, nonatomic) NSString* protocol; + +/** If YES, it can be missing in the input data, and the input would be still valid */ +@property (assign, nonatomic) BOOL isOptional; + +/** If YES - don't call any transformers on this property's value */ +@property (assign, nonatomic) BOOL isStandardJSONType; + +/** If YES - create a mutable object for the value of the property */ +@property (assign, nonatomic) BOOL isMutable; + +/** If YES - create models on demand for the array members */ +@property (assign, nonatomic) BOOL convertsOnDemand; + +/** If YES - the value of this property determines equality to other models */ +@property (assign, nonatomic) BOOL isIndex; + +/** The status of property getter introspection in a model */ +@property (assign, nonatomic) PropertyGetterType getterType; + +/** a custom getter for this property, found in the owning model */ +@property (assign, nonatomic) SEL customGetter; + +/** custom setters for this property, found in the owning model */ +@property (strong, nonatomic) NSMutableDictionary *customSetters; + +@end diff --git a/JSONModel/JSONModel/JSONModelClassProperty.m b/JSONModel/JSONModel/JSONModelClassProperty.m new file mode 100644 index 0000000..827de0a --- /dev/null +++ b/JSONModel/JSONModel/JSONModelClassProperty.m @@ -0,0 +1,60 @@ +// +// JSONModelClassProperty.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModelClassProperty.h" + +@implementation JSONModelClassProperty + +-(NSString*)description +{ + //build the properties string for the current class property + NSMutableArray* properties = [NSMutableArray arrayWithCapacity:8]; + + if (self.isIndex) [properties addObject:@"Index"]; + if (self.isOptional) [properties addObject:@"Optional"]; + if (self.isMutable) [properties addObject:@"Mutable"]; + if (self.convertsOnDemand) [properties addObject:@"ConvertOnDemand"]; + if (self.isStandardJSONType) [properties addObject:@"Standard JSON type"]; + if (self.customGetter) [properties addObject:[NSString stringWithFormat: @"Getter = %@", NSStringFromSelector(self.customGetter)]]; + + if (self.customSetters) + { + NSMutableArray *setters = [NSMutableArray array]; + + for (id obj in self.customSetters.allValues) + { + if (obj != [NSNull null]) + [setters addObject:obj]; + } + + [properties addObject:[NSString stringWithFormat: @"Setters = [%@]", [setters componentsJoinedByString:@", "]]]; + } + + NSString* propertiesString = @""; + if (properties.count>0) { + propertiesString = [NSString stringWithFormat:@"(%@)", [properties componentsJoinedByString:@", "]]; + } + + //return the name, type and additional properties + return [NSString stringWithFormat:@"@property %@%@ %@ %@", + self.type?[NSString stringWithFormat:@"%@*",self.type]:(self.structName?self.structName:@"primitive"), + self.protocol?[NSString stringWithFormat:@"<%@>", self.protocol]:@"", + self.name, + propertiesString + ]; +} + +@end diff --git a/JSONModel/JSONModel/JSONModelError.h b/JSONModel/JSONModel/JSONModelError.h new file mode 100644 index 0000000..10c6b0f --- /dev/null +++ b/JSONModel/JSONModel/JSONModelError.h @@ -0,0 +1,114 @@ +// +// JSONModelError.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +///////////////////////////////////////////////////////////////////////////////////////////// +typedef NS_ENUM(int, kJSONModelErrorTypes) +{ + kJSONModelErrorInvalidData = 1, + kJSONModelErrorBadResponse = 2, + kJSONModelErrorBadJSON = 3, + kJSONModelErrorModelIsInvalid = 4, + kJSONModelErrorNilInput = 5 +}; + +///////////////////////////////////////////////////////////////////////////////////////////// +/** The domain name used for the JSONModelError instances */ +extern NSString* const JSONModelErrorDomain; + +/** + * If the model JSON input misses keys that are required, check the + * userInfo dictionary of the JSONModelError instance you get back - + * under the kJSONModelMissingKeys key you will find a list of the + * names of the missing keys. + */ +extern NSString* const kJSONModelMissingKeys; + +/** + * If JSON input has a different type than expected by the model, check the + * userInfo dictionary of the JSONModelError instance you get back - + * under the kJSONModelTypeMismatch key you will find a description + * of the mismatched types. + */ +extern NSString* const kJSONModelTypeMismatch; + +/** + * If an error occurs in a nested model, check the userInfo dictionary of + * the JSONModelError instance you get back - under the kJSONModelKeyPath + * key you will find key-path at which the error occurred. + */ +extern NSString* const kJSONModelKeyPath; + +///////////////////////////////////////////////////////////////////////////////////////////// +/** + * Custom NSError subclass with shortcut methods for creating + * the common JSONModel errors + */ +@interface JSONModelError : NSError + +@property (strong, nonatomic) NSHTTPURLResponse* httpResponse; + +@property (strong, nonatomic) NSData* responseData; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorInvalidData = 1 + */ ++(id)errorInvalidDataWithMessage:(NSString*)message; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorInvalidData = 1 + * @param keys a set of field names that were required, but not found in the input + */ ++(id)errorInvalidDataWithMissingKeys:(NSSet*)keys; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorInvalidData = 1 + * @param mismatchDescription description of the type mismatch that was encountered. + */ ++(id)errorInvalidDataWithTypeMismatch:(NSString*)mismatchDescription; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorBadResponse = 2 + */ ++(id)errorBadResponse; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorBadJSON = 3 + */ ++(id)errorBadJSON; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorModelIsInvalid = 4 + */ ++(id)errorModelIsInvalid; + +/** + * Creates a JSONModelError instance with code kJSONModelErrorNilInput = 5 + */ ++(id)errorInputIsNil; + +/** + * Creates a new JSONModelError with the same values plus information about the key-path of the error. + * Properties in the new error object are the same as those from the receiver, + * except that a new key kJSONModelKeyPath is added to the userInfo dictionary. + * This key contains the component string parameter. If the key is already present + * then the new error object has the component string prepended to the existing value. + */ +- (instancetype)errorByPrependingKeyPathComponent:(NSString*)component; + +///////////////////////////////////////////////////////////////////////////////////////////// +@end diff --git a/JSONModel/JSONModel/JSONModelError.m b/JSONModel/JSONModel/JSONModelError.m new file mode 100644 index 0000000..ffbf2fb --- /dev/null +++ b/JSONModel/JSONModel/JSONModelError.m @@ -0,0 +1,93 @@ +// +// JSONModelError.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModelError.h" + +NSString* const JSONModelErrorDomain = @"JSONModelErrorDomain"; +NSString* const kJSONModelMissingKeys = @"kJSONModelMissingKeys"; +NSString* const kJSONModelTypeMismatch = @"kJSONModelTypeMismatch"; +NSString* const kJSONModelKeyPath = @"kJSONModelKeyPath"; + +@implementation JSONModelError + ++(id)errorInvalidDataWithMessage:(NSString*)message +{ + message = [NSString stringWithFormat:@"Invalid JSON data: %@", message]; + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorInvalidData + userInfo:@{NSLocalizedDescriptionKey:message}]; +} + ++(id)errorInvalidDataWithMissingKeys:(NSSet *)keys +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorInvalidData + userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON data. Required JSON keys are missing from the input. Check the error user information.",kJSONModelMissingKeys:[keys allObjects]}]; +} + ++(id)errorInvalidDataWithTypeMismatch:(NSString*)mismatchDescription +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorInvalidData + userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON data. The JSON type mismatches the expected type. Check the error user information.",kJSONModelTypeMismatch:mismatchDescription}]; +} + ++(id)errorBadResponse +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorBadResponse + userInfo:@{NSLocalizedDescriptionKey:@"Bad network response. Probably the JSON URL is unreachable."}]; +} + ++(id)errorBadJSON +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorBadJSON + userInfo:@{NSLocalizedDescriptionKey:@"Malformed JSON. Check the JSONModel data input."}]; +} + ++(id)errorModelIsInvalid +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorModelIsInvalid + userInfo:@{NSLocalizedDescriptionKey:@"Model does not validate. The custom validation for the input data failed."}]; +} + ++(id)errorInputIsNil +{ + return [JSONModelError errorWithDomain:JSONModelErrorDomain + code:kJSONModelErrorNilInput + userInfo:@{NSLocalizedDescriptionKey:@"Initializing model with nil input object."}]; +} + +- (instancetype)errorByPrependingKeyPathComponent:(NSString*)component +{ + // Create a mutable copy of the user info so that we can add to it and update it + NSMutableDictionary* userInfo = [self.userInfo mutableCopy]; + + // Create or update the key-path + NSString* existingPath = userInfo[kJSONModelKeyPath]; + NSString* separator = [existingPath hasPrefix:@"["] ? @"" : @"."; + NSString* updatedPath = (existingPath == nil) ? component : [component stringByAppendingFormat:@"%@%@", separator, existingPath]; + userInfo[kJSONModelKeyPath] = updatedPath; + + // Create the new error + return [JSONModelError errorWithDomain:self.domain + code:self.code + userInfo:[NSDictionary dictionaryWithDictionary:userInfo]]; +} + +@end diff --git a/JSONModel/JSONModelCategories/NSArray+JSONModel.h b/JSONModel/JSONModelCategories/NSArray+JSONModel.h new file mode 100644 index 0000000..3e50cdf --- /dev/null +++ b/JSONModel/JSONModelCategories/NSArray+JSONModel.h @@ -0,0 +1,40 @@ +// +// NSArray+JSONModel.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +#import +#import "JSONModel.h" + +/** + * Exposes invisible JSONModelArray methods + */ +@interface NSArray(JSONModel) + +/** + * Looks up the array's contents and tries to find a JSONModel object + * with matching index property value to the indexValue param. + * + * Will return nil if no matching model is found. Will return nil if there's no index property + * defined on the models found in the array (will sample the first object, assuming the array + * contains homogeneous collection of objects) + * + * @param indexValue the id value to search for + * @return the found model or nil + * @exception NSException throws exception if you call this method on an instance, which is not actually a JSONModelArray + */ +- (id)modelWithIndexValue:(id)indexValue; + +@end diff --git a/JSONModel/JSONModelCategories/NSArray+JSONModel.m b/JSONModel/JSONModelCategories/NSArray+JSONModel.m new file mode 100644 index 0000000..cc9fdc8 --- /dev/null +++ b/JSONModel/JSONModelCategories/NSArray+JSONModel.m @@ -0,0 +1,28 @@ +// +// NSArray+JSONModel.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +#import "NSArray+JSONModel.h" + +@implementation NSArray(JSONModel) + +- (id)modelWithIndexValue:(id)indexValue +{ + NSAssert(NO, @"call modelWithIndexValue: on a ConvertOnDemand property, which is defined like that: @property (strong, nonatomic) NSArray* list;"); + return nil; +} + +@end diff --git a/JSONModel/JSONModelLib.h b/JSONModel/JSONModelLib.h new file mode 100644 index 0000000..204fef7 --- /dev/null +++ b/JSONModel/JSONModelLib.h @@ -0,0 +1,35 @@ +// +// JSONModelLib.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +//JSONModel transformations +#import "JSONValueTransformer.h" +#import "JSONKeyMapper.h" + +//basic JSONModel classes +#import "JSONModelError.h" +#import "JSONModelClassProperty.h" +#import "JSONModel.h" + +//network classes +#import "JSONHTTPClient.h" +#import "JSONModel+networking.h" +#import "JSONAPI.h" + +//models array +#import "NSArray+JSONModel.h" +#import "JSONModelArray.h" diff --git a/JSONModel/JSONModelNetworking/JSONAPI.h b/JSONModel/JSONModelNetworking/JSONAPI.h new file mode 100644 index 0000000..6a86cf7 --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONAPI.h @@ -0,0 +1,90 @@ +// +// JSONAPI.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import +#import "JSONHTTPClient.h" + +///////////////////////////////////////////////////////////////////////////////////////////// + +/** + * @discussion Class for working with JSON APIs. It builds upon the JSONHTTPClient class + * and facilitates making requests to the same web host. Also features helper + * method for making calls to a JSON RPC service + */ +@interface JSONAPI : NSObject + +///////////////////////////////////////////////////////////////////////////////////////////// + +/** @name Configuring the API */ +/** + * Sets the API url + * @param base the API url as a string + */ ++(void)setAPIBaseURLWithString:(NSString*)base; + +/** + * Sets the default content type for the requests/responses + * @param ctype The content-type as a string. Some possible types, + * depending on the service: application/json, text/json, x-application/javascript, etc. + */ ++(void)setContentType:(NSString*)ctype; + +///////////////////////////////////////////////////////////////////////////////////////////// + +/** @name Making GET API requests */ +/** + * Makes an asynchronous GET request to the API + * @param path the URL path to add to the base API URL for this HTTP call + * @param params the variables to pass to the API + * @param completeBlock a JSONObjectBlock block to execute upon completion + */ ++(void)getWithPath:(NSString*)path andParams:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; + +///////////////////////////////////////////////////////////////////////////////////////////// + +/** @name Making POST API requests */ +/** + * Makes a POST request to the API + * @param path the URL path to add to the base API URL for this HTTP call + * @param params the variables to pass to the API + * @param completeBlock a JSONObjectBlock block to execute upon completion + */ ++(void)postWithPath:(NSString*)path andParams:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; + +///////////////////////////////////////////////////////////////////////////////////////////// + +/** @name JSON RPC methods */ +/** + * Makes an asynchronous JSON RPC request to the API. Read more: http://www.jsonrpc.org + * @param method the HTTP method name; GET or POST only + * @param args the list of arguments to pass to the API + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)rpcWithMethodName:(NSString*)method andArguments:(NSArray*)args completion:(JSONObjectBlock)completeBlock; + +/** @name JSON RPC (2.0) request method */ +/** + * Makes an asynchronous JSON RPC 2.0 request to the API. Read more: http://www.jsonrpc.org + * @param method the HTTP method name; GET or POST only + * @param params the params to pass to the API - an NSArray or an NSDictionary, + * depending whether you're using named or unnamed parameters + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)rpc2WithMethodName:(NSString*)method andParams:(id)params completion:(JSONObjectBlock)completeBlock; + +///////////////////////////////////////////////////////////////////////////////////////////// + +@end diff --git a/JSONModel/JSONModelNetworking/JSONAPI.m b/JSONModel/JSONModelNetworking/JSONAPI.m new file mode 100644 index 0000000..a05b44d --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONAPI.m @@ -0,0 +1,152 @@ +// +// JSONAPI.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONAPI.h" + +#pragma mark - helper error model class +@interface JSONAPIRPCErrorModel: JSONModel +@property (assign, nonatomic) int code; +@property (strong, nonatomic) NSString* message; +@property (strong, nonatomic) id data; +@end + +#pragma mark - static variables + +static JSONAPI* sharedInstance = nil; +static long jsonRpcId = 0; + +#pragma mark - JSONAPI() private interface + +@interface JSONAPI () +@property (strong, nonatomic) NSString* baseURLString; +@end + +#pragma mark - JSONAPI implementation + +@implementation JSONAPI + +#pragma mark - initialize + ++(void)initialize +{ + static dispatch_once_t once; + dispatch_once(&once, ^{ + sharedInstance = [[JSONAPI alloc] init]; + }); +} + +#pragma mark - api config methods + ++(void)setAPIBaseURLWithString:(NSString*)base +{ + sharedInstance.baseURLString = base; +} + ++(void)setContentType:(NSString*)ctype +{ + [JSONHTTPClient setRequestContentType: ctype]; +} + +#pragma mark - GET methods ++(void)getWithPath:(NSString*)path andParams:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock +{ + NSString* fullURL = [NSString stringWithFormat:@"%@%@", sharedInstance.baseURLString, path]; + + [JSONHTTPClient getJSONFromURLWithString: fullURL params:params completion:^(NSDictionary *json, JSONModelError *e) { + completeBlock(json, e); + }]; +} + +#pragma mark - POST methods ++(void)postWithPath:(NSString*)path andParams:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock +{ + NSString* fullURL = [NSString stringWithFormat:@"%@%@", sharedInstance.baseURLString, path]; + + [JSONHTTPClient postJSONFromURLWithString: fullURL params:params completion:^(NSDictionary *json, JSONModelError *e) { + completeBlock(json, e); + }]; +} + +#pragma mark - RPC methods ++(void)__rpcRequestWithObject:(id)jsonObject completion:(JSONObjectBlock)completeBlock +{ + + NSData* jsonRequestData = [NSJSONSerialization dataWithJSONObject:jsonObject + options:kNilOptions + error:nil]; + NSString* jsonRequestString = [[NSString alloc] initWithData:jsonRequestData encoding: NSUTF8StringEncoding]; + + NSAssert(sharedInstance.baseURLString, @"API base URL not set"); + [JSONHTTPClient postJSONFromURLWithString: sharedInstance.baseURLString + bodyString: jsonRequestString + completion:^(NSDictionary *json, JSONModelError* e) { + + if (completeBlock) { + //handle the rpc response + NSDictionary* result = json[@"result"]; + + if (!result) { + JSONAPIRPCErrorModel* error = [[JSONAPIRPCErrorModel alloc] initWithDictionary:json[@"error"] error:nil]; + if (error) { + //custom server error + if (!error.message) error.message = @"Generic json rpc error"; + e = [JSONModelError errorWithDomain:JSONModelErrorDomain + code:error.code + userInfo: @{ NSLocalizedDescriptionKey : error.message}]; + } else { + //generic error + e = [JSONModelError errorBadResponse]; + } + } + + //invoke the callback + completeBlock(result, e); + } + }]; +} + ++(void)rpcWithMethodName:(NSString*)method andArguments:(NSArray*)args completion:(JSONObjectBlock)completeBlock +{ + NSAssert(method, @"No method specified"); + if (!args) args = @[]; + + [self __rpcRequestWithObject:@{ + //rpc 1.0 + @"id": @(++jsonRpcId), + @"params": args, + @"method": method + } completion:completeBlock]; +} + ++(void)rpc2WithMethodName:(NSString*)method andParams:(id)params completion:(JSONObjectBlock)completeBlock +{ + NSAssert(method, @"No method specified"); + if (!params) params = @[]; + + [self __rpcRequestWithObject:@{ + //rpc 2.0 + @"jsonrpc": @"2.0", + @"id": @(++jsonRpcId), + @"params": params, + @"method": method + } completion:completeBlock]; +} + +@end + +#pragma mark - helper rpc error model class implementation +@implementation JSONAPIRPCErrorModel +@end diff --git a/JSONModel/JSONModelNetworking/JSONHTTPClient.h b/JSONModel/JSONModelNetworking/JSONHTTPClient.h new file mode 100644 index 0000000..7b2e007 --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONHTTPClient.h @@ -0,0 +1,175 @@ +// +// JSONModelHTTPClient.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModel.h" + +#pragma mark - definitions + +/** + * HTTP Request methods + */ +extern NSString* const kHTTPMethodGET; +extern NSString* const kHTTPMethodPOST; + +/** + * Content-type strings + */ +extern NSString* const kContentTypeAutomatic; +extern NSString* const kContentTypeJSON; +extern NSString* const kContentTypeWWWEncoded; + +/** + * A block type to handle incoming JSON object and an error. + * You pass it to methods which fetch JSON asynchronously. When the operation is finished + * you receive back the fetched JSON (or nil) and an error (or nil) + * + * @param json object derived from a JSON string + * @param err JSONModelError or nil + */ +typedef void (^JSONObjectBlock)(id json, JSONModelError* err); + +///////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - configuration methods + +/** + * @discussion A very thin HTTP client that can do GET and POST HTTP requests. + * It fetches only JSON data and also deserializes it using NSJSONSerialization. + */ +@interface JSONHTTPClient : NSObject + +///////////////////////////////////////////////////////////////////////////////////////////// + + +/** @name HTTP Client configuration */ +/** + * Returns a modifiable dictionary of the client's default HTTP headers. + * @result A mutable dictionary of pairs - HTTP header names and values. + * @discussion You can use the result to modify the http client headers like so: + *
+ * NSMutableDictionary* headers = [JSONHTTPClient requestHeaders];
+ * headers[@"APIToken"] = @"MySecretTokenValue";
+ * 
+ */ ++(NSMutableDictionary*)requestHeaders; + +/** + * Sets the default encoding of the request body. + * See NSStringEncoding for a list of supported encodings + * @param encoding text encoding constant + */ ++(void)setDefaultTextEncoding:(NSStringEncoding)encoding; + +/** + * Sets the policies for caching HTTP data + * See NSURLRequestCachePolicy for a list of the pre-defined policies + * @param policy the caching policy + */ ++(void)setCachingPolicy:(NSURLRequestCachePolicy)policy; + +/** + * Sets the timeout for network calls + * @param seconds the amount of seconds to wait before considering the call failed + */ ++(void)setTimeoutInSeconds:(int)seconds; + +/** + * A method to set the default content type of the request body + * By default the content type is set to kContentTypeAutomatic + * which checks the body request and decides between "application/json" + * and "application/x-www-form-urlencoded" + */ ++(void)setRequestContentType:(NSString*)contentTypeString; + +///////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - GET asynchronous JSON calls + +/** @name Making asynchronous HTTP requests */ +/** + * Makes GET request to the given URL address and fetches a JSON response. + * @param urlString the URL as a string + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock; + +/** + * Makes GET request to the given URL address and fetches a JSON response. Sends the params as a query string variables. + * @param urlString the URL as a string + * @param params a dictionary of key / value pairs to be send as variables to the request + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; + +/** + * Makes a request to the given URL address and fetches a JSON response. + * @param urlString the URL as a string + * @param method the method of the request as a string + * @param params a dictionary of key / value pairs to be send as variables to the request + * @param bodyString the body of the POST request as a string + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock; + +/** + * Makes a request to the given URL address and fetches a JSON response. + * @param urlString the URL as a string + * @param method the method of the request as a string + * @param params a dictionary of key / value pairs to be send as variables to the request + * @param bodyString the body of the POST request as a string + * @param headers the headers to set on the request - overrides those in +requestHeaders + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock; + +/** + * Makes a request to the given URL address and fetches a JSON response. + * @param urlString the URL as a string + * @param method the method of the request as a string + * @param params a dictionary of key / value pairs to be send as variables to the request + * @param bodyData the body of the POST request as raw binary data + * @param headers the headers to set on the request - overrides those in +requestHeaders + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary *)params orBodyData:(NSData*)bodyData headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock; + +///////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - POST asynchronous JSON calls + +/** + * Makes POST request to the given URL address and fetches a JSON response. Sends the bodyString param as the POST request body. + * @param urlString the URL as a string + * @param params a dictionary of key / value pairs to be send as variables to the request + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; + +/** + * Makes POST request to the given URL address and fetches a JSON response. Sends the bodyString param as the POST request body. + * @param urlString the URL as a string + * @param bodyString the body of the POST request as a string + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock; + +/** + * Makes POST request to the given URL address and fetches a JSON response. Sends the bodyString param as the POST request body. + * @param urlString the URL as a string + * @param bodyData the body of the POST request as an NSData object + * @param completeBlock JSONObjectBlock to execute upon completion + */ ++(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock; + + +@end diff --git a/JSONModel/JSONModelNetworking/JSONHTTPClient.m b/JSONModel/JSONModelNetworking/JSONHTTPClient.m new file mode 100644 index 0000000..b3499a5 --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONHTTPClient.m @@ -0,0 +1,369 @@ +// +// JSONModelHTTPClient.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONHTTPClient.h" + +typedef void (^RequestResultBlock)(NSData *data, JSONModelError *error); + +#pragma mark - constants +NSString* const kHTTPMethodGET = @"GET"; +NSString* const kHTTPMethodPOST = @"POST"; + +NSString* const kContentTypeAutomatic = @"jsonmodel/automatic"; +NSString* const kContentTypeJSON = @"application/json"; +NSString* const kContentTypeWWWEncoded = @"application/x-www-form-urlencoded"; + +#pragma mark - static variables + +/** + * Defaults for HTTP requests + */ +static NSStringEncoding defaultTextEncoding = NSUTF8StringEncoding; +static NSURLRequestCachePolicy defaultCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + +static int defaultTimeoutInSeconds = 60; + +/** + * Custom HTTP headers to send over with *each* request + */ +static NSMutableDictionary* requestHeaders = nil; + +/** + * Default request content type + */ +static NSString* requestContentType = nil; + +#pragma mark - implementation +@implementation JSONHTTPClient + +#pragma mark - initialization ++(void)initialize +{ + static dispatch_once_t once; + dispatch_once(&once, ^{ + requestHeaders = [NSMutableDictionary dictionary]; + requestContentType = kContentTypeAutomatic; + }); +} + +#pragma mark - configuration methods ++(NSMutableDictionary*)requestHeaders +{ + return requestHeaders; +} + ++(void)setDefaultTextEncoding:(NSStringEncoding)encoding +{ + defaultTextEncoding = encoding; +} + ++(void)setCachingPolicy:(NSURLRequestCachePolicy)policy +{ + defaultCachePolicy = policy; +} + ++(void)setTimeoutInSeconds:(int)seconds +{ + defaultTimeoutInSeconds = seconds; +} + ++(void)setRequestContentType:(NSString*)contentTypeString +{ + requestContentType = contentTypeString; +} + +#pragma mark - helper methods ++(NSString*)contentTypeForRequestString:(NSString*)requestString +{ + //fetch the charset name from the default string encoding + NSString* contentType = requestContentType; + + if (requestString.length>0 && [contentType isEqualToString:kContentTypeAutomatic]) { + //check for "eventual" JSON array or dictionary + NSString* firstAndLastChar = [NSString stringWithFormat:@"%@%@", + [requestString substringToIndex:1], + [requestString substringFromIndex: requestString.length -1] + ]; + + if ([firstAndLastChar isEqualToString:@"{}"] || [firstAndLastChar isEqualToString:@"[]"]) { + //guessing for a JSON request + contentType = kContentTypeJSON; + } else { + //fallback to www form encoded params + contentType = kContentTypeWWWEncoded; + } + } + + //type is set, just add charset + NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); + return [NSString stringWithFormat:@"%@; charset=%@", contentType, charset]; +} + ++(NSString*)urlEncode:(id)value +{ + //make sure param is a string + if ([value isKindOfClass:[NSNumber class]]) { + value = [(NSNumber*)value stringValue]; + } + + NSAssert([value isKindOfClass:[NSString class]], @"request parameters can be only of NSString or NSNumber classes. '%@' is of class %@.", value, [value class]); + + NSString *str = (NSString *)value; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 + return [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + +#else + return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes( + NULL, + (__bridge CFStringRef)str, + NULL, + (CFStringRef)@"!*'();:@&=+$,/?%#[]", + kCFStringEncodingUTF8)); +#endif +} + +#pragma mark - networking worker methods ++(void)requestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers handler:(RequestResultBlock)handler +{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url + cachePolicy: defaultCachePolicy + timeoutInterval: defaultTimeoutInSeconds]; + [request setHTTPMethod:method]; + + if ([requestContentType isEqualToString:kContentTypeAutomatic]) { + //automatic content type + if (bodyData) { + NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding]; + [request setValue: [self contentTypeForRequestString: bodyString] forHTTPHeaderField:@"Content-type"]; + } + } else { + //user set content type + [request setValue: requestContentType forHTTPHeaderField:@"Content-type"]; + } + + //add all the custom headers defined + for (NSString* key in [requestHeaders allKeys]) { + [request setValue:requestHeaders[key] forHTTPHeaderField:key]; + } + + //add the custom headers + for (NSString* key in [headers allKeys]) { + [request setValue:headers[key] forHTTPHeaderField:key]; + } + + if (bodyData) { + [request setHTTPBody: bodyData]; + [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)bodyData.length] forHTTPHeaderField:@"Content-Length"]; + } + + void (^completionHandler)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *origResponse, NSError *origError) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)origResponse; + JSONModelError *error = nil; + + //convert an NSError to a JSONModelError + if (origError) { + error = [JSONModelError errorWithDomain:origError.domain code:origError.code userInfo:origError.userInfo]; + } + + //special case for http error code 401 + if (error.code == NSURLErrorUserCancelledAuthentication) { + response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:401 HTTPVersion:@"HTTP/1.1" headerFields:@{}]; + } + + //if not OK status set the err to a JSONModelError instance + if (!error && (response.statusCode >= 300 || response.statusCode < 200)) { + error = [JSONModelError errorBadResponse]; + } + + //if there was an error, assign the response to the JSONModel instance + if (error) { + error.httpResponse = [response copy]; + } + + //empty respone, return nil instead + if (!data.length) { + data = nil; + } + + handler(data, error); + }; + + //fire the request + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_10 + NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completionHandler]; + [task resume]; +#else + NSOperationQueue *queue = [NSOperationQueue new]; + + [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + completionHandler(data, response, error); + }]; +#endif +} + ++(void)requestDataFromURL:(NSURL*)url method:(NSString*)method params:(NSDictionary*)params headers:(NSDictionary*)headers handler:(RequestResultBlock)handler +{ + //create the request body + NSMutableString* paramsString = nil; + + if (params) { + //build a simple url encoded param string + paramsString = [NSMutableString stringWithString:@""]; + for (NSString* key in [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [paramsString appendFormat:@"%@=%@&", key, [self urlEncode:params[key]] ]; + } + if ([paramsString hasSuffix:@"&"]) { + paramsString = [[NSMutableString alloc] initWithString: [paramsString substringToIndex: paramsString.length-1]]; + } + } + + //set the request params + if ([method isEqualToString:kHTTPMethodGET] && params) { + + //add GET params to the query string + url = [NSURL URLWithString:[NSString stringWithFormat: @"%@%@%@", + [url absoluteString], + [url query] ? @"&" : @"?", + paramsString + ]]; + } + + //call the more general synq request method + [self requestDataFromURL: url + method: method + requestBody: [method isEqualToString:kHTTPMethodPOST]?[paramsString dataUsingEncoding:NSUTF8StringEncoding]:nil + headers: headers + handler:handler]; +} + +#pragma mark - Async network request ++(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString + method:method + params:params + orBodyString:bodyString + headers:nil + completion:completeBlock]; +} + ++(void)JSONFromURLWithString:(NSString *)urlString method:(NSString *)method params:(NSDictionary *)params orBodyString:(NSString *)bodyString headers:(NSDictionary *)headers completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString + method:method + params:params + orBodyData:[bodyString dataUsingEncoding:NSUTF8StringEncoding] + headers:headers + completion:completeBlock]; +} + ++(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary *)params orBodyData:(NSData*)bodyData headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock +{ + RequestResultBlock handler = ^(NSData *responseData, JSONModelError *error) { + id jsonObject = nil; + + //step 3: if there's no response so far, return a basic error + if (!responseData && !error) { + //check for false response, but no network error + error = [JSONModelError errorBadResponse]; + } + + //step 4: if there's a response at this and no errors, convert to object + if (error==nil) { + // Note: it is possible to have a valid response with empty response data (204 No Content). + // So only create the JSON object if there is some response data. + if(responseData.length > 0) + { + //convert to an object + jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error]; + } + } + //step 4.5: cover an edge case in which meaningful content is return along an error HTTP status code + else if (error && responseData && jsonObject==nil) { + //try to get the JSON object, while preserving the original error object + jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil]; + //keep responseData just in case it contains error information + error.responseData = responseData; + } + + //step 5: invoke the complete block + dispatch_async(dispatch_get_main_queue(), ^{ + if (completeBlock) { + completeBlock(jsonObject, error); + } + }); + }; + + NSURL *url = [NSURL URLWithString:urlString]; + + if (bodyData) { + [self requestDataFromURL:url method:method requestBody:bodyData headers:headers handler:handler]; + } else { + [self requestDataFromURL:url method:method params:params headers:headers handler:handler]; + } +} + +#pragma mark - request aliases ++(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString method:kHTTPMethodGET + params:nil + orBodyString:nil completion:^(id json, JSONModelError* e) { + if (completeBlock) completeBlock(json, e); + }]; +} + ++(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString method:kHTTPMethodGET + params:params + orBodyString:nil completion:^(id json, JSONModelError* e) { + if (completeBlock) completeBlock(json, e); + }]; +} + ++(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString method:kHTTPMethodPOST + params:params + orBodyString:nil completion:^(id json, JSONModelError* e) { + if (completeBlock) completeBlock(json, e); + }]; + +} + ++(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString method:kHTTPMethodPOST + params:nil + orBodyString:bodyString completion:^(id json, JSONModelError* e) { + if (completeBlock) completeBlock(json, e); + }]; +} + ++(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock +{ + [self JSONFromURLWithString:urlString method:kHTTPMethodPOST + params:nil + orBodyString:[[NSString alloc] initWithData:bodyData encoding:defaultTextEncoding] + completion:^(id json, JSONModelError* e) { + if (completeBlock) completeBlock(json, e); + }]; +} + +@end diff --git a/JSONModel/JSONModelNetworking/JSONModel+networking.h b/JSONModel/JSONModelNetworking/JSONModel+networking.h new file mode 100644 index 0000000..9529d77 --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONModel+networking.h @@ -0,0 +1,66 @@ +// +// JSONModel+networking.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModel.h" +#import "JSONHTTPClient.h" + +typedef void (^JSONModelBlock)(id model, JSONModelError* err); + +/** + * The JSONModel(networking) class category adds networking to JSONModel. + * It adds initFromURLWithString: initializer, which makes a GET http request + * to the URL given and initializes the model with the returned JSON. + * Use #import "JSONModel+networking.h" to import networking capabilities. + */ +@interface JSONModel(Networking) + +@property (assign, nonatomic) BOOL isLoading; +/** @name Asynchronously create a model over the network */ +/** + * Asynchronously create a model over the network. Create a new model instance and initialize it with the JSON fetched from the given URL + * @param urlString the absolute URL address of the JSON feed as a string + * @param completeBlock JSONModelBlock executed upon completion. The JSONModelBlock type is defined as: void (^JSONModelBlock)(JSONModel* model, JSONModelError* e); the first parameter is the initialized model or nil, + * and second parameter holds the model initialization error, if any + */ +-(instancetype)initFromURLWithString:(NSString *)urlString completion:(JSONModelBlock)completeBlock; + +/** + * Asynchronously gets the contents of a URL and constructs a JSONModel object from the response. + * The constructed JSONModel object passed as the first parameter to the completion block will be of the same + * class as the receiver. So call this method on yourJSONModel sub-class rather than directly on JSONModel. + * @param urlString The absolute URL of the JSON resource, as a string + * @param completeBlock The block to be called upon completion. + * JSONModelBlock type is defined as: void (^JSONModelBlock)(JSONModel* model, JSONModelError* err); + * The first parameter is the initialized model (of the same JSONModel sub-class as the receiver) or nil if there was an error; + * The second parameter is the initialization error, if any. + */ ++ (void)getModelFromURLWithString:(NSString*)urlString completion:(JSONModelBlock)completeBlock; + +/** + * Asynchronously posts a JSONModel object (as JSON) to a URL and constructs a JSONModel object from the response. + * The constructed JSONModel object passed as the first parameter to the completion block will be of the same + * class as the receiver. So call this method on yourJSONModel sub-class rather than directly on JSONModel. + * @param post A JSONModel object that will be converted to JSON and sent as the POST data to the HTTP request. + * @param urlString The absolute URL of the JSON resource, as a string + * @param completeBlock The block to be called upon completion. + * JSONModelBlock type is defined as: void (^JSONModelBlock)(JSONModel* model, JSONModelError* err); + * The first parameter is the initialized model (of the same JSONModel sub-class as the receiver) or nil if there was an error; + * The second parameter is the initialization error, if any. + */ ++ (void)postModel:(JSONModel*)post toURLWithString:(NSString*)urlString completion:(JSONModelBlock)completeBlock; + + +@end diff --git a/JSONModel/JSONModelNetworking/JSONModel+networking.m b/JSONModel/JSONModelNetworking/JSONModel+networking.m new file mode 100644 index 0000000..bb070c4 --- /dev/null +++ b/JSONModel/JSONModelNetworking/JSONModel+networking.m @@ -0,0 +1,109 @@ +// +// JSONModel+networking.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONModel+networking.h" +#import "JSONHTTPClient.h" + +BOOL _isLoading; + +@implementation JSONModel(Networking) + +@dynamic isLoading; + +-(BOOL)isLoading +{ + return _isLoading; +} + +-(void)setIsLoading:(BOOL)isLoading +{ + _isLoading = isLoading; +} + +-(instancetype)initFromURLWithString:(NSString *)urlString completion:(JSONModelBlock)completeBlock +{ + id placeholder = [super init]; + __block id blockSelf = self; + + if (placeholder) { + //initialization + self.isLoading = YES; + + [JSONHTTPClient getJSONFromURLWithString:urlString + completion:^(NSDictionary *json, JSONModelError* e) { + + JSONModelError* initError = nil; + blockSelf = [self initWithDictionary:json error:&initError]; + + if (completeBlock) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + completeBlock(blockSelf, e?e:initError ); + }); + } + + self.isLoading = NO; + + }]; + } + return placeholder; +} + ++ (void)getModelFromURLWithString:(NSString*)urlString completion:(JSONModelBlock)completeBlock +{ + [JSONHTTPClient getJSONFromURLWithString:urlString + completion:^(NSDictionary* jsonDict, JSONModelError* err) + { + JSONModel* model = nil; + + if(err == nil) + { + model = [[self alloc] initWithDictionary:jsonDict error:&err]; + } + + if(completeBlock != nil) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + completeBlock(model, err); + }); + } + }]; +} + ++ (void)postModel:(JSONModel*)post toURLWithString:(NSString*)urlString completion:(JSONModelBlock)completeBlock +{ + [JSONHTTPClient postJSONFromURLWithString:urlString + bodyString:[post toJSONString] + completion:^(NSDictionary* jsonDict, JSONModelError* err) + { + JSONModel* model = nil; + + if(err == nil) + { + model = [[self alloc] initWithDictionary:jsonDict error:&err]; + } + + if(completeBlock != nil) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + completeBlock(model, err); + }); + } + }]; +} + +@end diff --git a/JSONModel/JSONModelTransformations/JSONKeyMapper.h b/JSONModel/JSONModelTransformations/JSONKeyMapper.h new file mode 100644 index 0000000..e022e5b --- /dev/null +++ b/JSONModel/JSONModelTransformations/JSONKeyMapper.h @@ -0,0 +1,102 @@ +// +// JSONKeyMapper.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import + +typedef NSString* (^JSONModelKeyMapBlock)(NSString* keyName); + +/** + * **You won't need to create or store instances of this class yourself.** If you want your model + * to have different property names than the JSON feed keys, look below on how to + * make your model use a key mapper. + * + * For example if you consume JSON from twitter + * you get back underscore_case style key names. For example: + * + *
"profile_sidebar_border_color": "0094C2",
+ * "profile_background_tile": false,
+ * + * To comply with Obj-C accepted camelCase property naming for your classes, + * you need to provide mapping between JSON keys and ObjC property names. + * + * In your model overwrite the +(JSONKeyMapper*)keyMapper method and provide a JSONKeyMapper + * instance to convert the key names for your model. + * + * If you need custom mapping it's as easy as: + *
+ * +(JSONKeyMapper*)keyMapper {
+ *   return [[JSONKeyMapper alloc] initWithDictionary:@{@"crazy_JSON_name":@"myCamelCaseName"}];
+ * }
+ * 
+ * In case you want to handle underscore_case, **use the predefined key mapper**, like so: + *
+ * +(JSONKeyMapper*)keyMapper {
+ *   return [JSONKeyMapper mapperFromUnderscoreCaseToCamelCase];
+ * }
+ * 
+ */ +@interface JSONKeyMapper : NSObject + +/** @name Name converters */ +/** Block, which takes in a JSON key and converts it to the corresponding property name */ +@property (readonly, nonatomic) JSONModelKeyMapBlock JSONToModelKeyBlock; + +/** Block, which takes in a property name and converts it to the corresponding JSON key name */ +@property (readonly, nonatomic) JSONModelKeyMapBlock modelToJSONKeyBlock; + +/** Combined converter method +* @param value the source name +* @param importing YES invokes JSONToModelKeyBlock, NO - modelToJSONKeyBlock +* @return JSONKeyMapper instance +*/ +-(NSString*)convertValue:(NSString*)value isImportingToModel:(BOOL)importing; + +/** @name Creating a key mapper */ + +/** + * Creates a JSONKeyMapper instance, based on the two blocks you provide this initializer. + * The two parameters take in a JSONModelKeyMapBlock block: + *
NSString* (^JSONModelKeyMapBlock)(NSString* keyName)
+ * The block takes in a string and returns the transformed (if at all) string. + * @param toModel transforms JSON key name to your model property name + * @param toJSON transforms your model property name to a JSON key + */ +-(instancetype)initWithJSONToModelBlock:(JSONModelKeyMapBlock)toModel + modelToJSONBlock:(JSONModelKeyMapBlock)toJSON; + +/** + * Creates a JSONKeyMapper instance, based on the mapping you provide + * in the map parameter. Use the JSON key names as keys, your JSONModel + * property names as values. + * @param map map dictionary, in the format:
@{@"crazy_JSON_name":@"myCamelCaseName"}
+ * @return JSONKeyMapper instance + */ +-(instancetype)initWithDictionary:(NSDictionary*)map; + +/** + * Creates a JSONKeyMapper, which converts underscore_case to camelCase and vice versa. + */ ++(instancetype)mapperFromUnderscoreCaseToCamelCase; + ++(instancetype)mapperFromUpperCaseToLowerCase; + +/** + * Creates a JSONKeyMapper based on a built-in JSONKeyMapper, with specific exceptions. + * Use the original JSON key names as keys, and your JSONModel property names as values. + */ ++ (instancetype)mapper:(JSONKeyMapper *)baseKeyMapper withExceptions:(NSDictionary *)exceptions; + +@end diff --git a/JSONModel/JSONModelTransformations/JSONKeyMapper.m b/JSONModel/JSONModelTransformations/JSONKeyMapper.m new file mode 100644 index 0000000..ee10794 --- /dev/null +++ b/JSONModel/JSONModelTransformations/JSONKeyMapper.m @@ -0,0 +1,237 @@ +// +// JSONKeyMapper.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONKeyMapper.h" +#import + +@interface JSONKeyMapper() +@property (nonatomic, strong) NSMutableDictionary *toModelMap; +@property (nonatomic, strong) NSMutableDictionary *toJSONMap; +@property (nonatomic, assign) OSSpinLock lock; +@end + +@implementation JSONKeyMapper + +-(instancetype)init +{ + self = [super init]; + if (self) { + //initialization + self.toModelMap = [NSMutableDictionary dictionary]; + self.toJSONMap = [NSMutableDictionary dictionary]; + } + return self; +} + +-(instancetype)initWithJSONToModelBlock:(JSONModelKeyMapBlock)toModel + modelToJSONBlock:(JSONModelKeyMapBlock)toJSON +{ + self = [self init]; + + if (self) { + + __weak JSONKeyMapper* weakSelf = self; + + _JSONToModelKeyBlock = [^NSString* (NSString* keyName) { + + __strong JSONKeyMapper* strongSelf = weakSelf; + + //try to return cached transformed key + if (strongSelf.toModelMap[keyName]) { + return strongSelf.toModelMap[keyName]; + } + + //try to convert the key, and store in the cache + NSString* result = toModel(keyName); + + OSSpinLockLock(&strongSelf->_lock); + strongSelf.toModelMap[keyName] = result; + OSSpinLockUnlock(&strongSelf->_lock); + + return result; + + } copy]; + + _modelToJSONKeyBlock = [^NSString* (NSString* keyName) { + + __strong JSONKeyMapper *strongSelf = weakSelf; + + //try to return cached transformed key + if (strongSelf.toJSONMap[keyName]) { + return strongSelf.toJSONMap[keyName]; + } + + //try to convert the key, and store in the cache + NSString* result = toJSON(keyName); + + OSSpinLockLock(&strongSelf->_lock); + strongSelf.toJSONMap[keyName] = result; + OSSpinLockUnlock(&strongSelf->_lock); + + return result; + + } copy]; + + } + + return self; +} + +-(instancetype)initWithDictionary:(NSDictionary *)map +{ + self = [super init]; + if (self) { + + NSDictionary *userToModelMap = [map copy]; + NSDictionary *userToJSONMap = [self swapKeysAndValuesInDictionary:map]; + + _JSONToModelKeyBlock = ^NSString *(NSString *keyName) { + NSString *result = [userToModelMap valueForKeyPath:keyName]; + return result ? result : keyName; + }; + + _modelToJSONKeyBlock = ^NSString *(NSString *keyName) { + NSString *result = [userToJSONMap valueForKeyPath:keyName]; + return result ? result : keyName; + }; + } + + return self; +} + +- (NSDictionary *)swapKeysAndValuesInDictionary:(NSDictionary *)dictionary +{ + NSMutableDictionary *swapped = [NSMutableDictionary new]; + + [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + NSAssert([value isKindOfClass:[NSString class]], @"Expect keys and values to be NSString"); + NSAssert([key isKindOfClass:[NSString class]], @"Expect keys and values to be NSString"); + swapped[value] = key; + }]; + + return swapped; +} + +-(NSString*)convertValue:(NSString*)value isImportingToModel:(BOOL)importing +{ + return !importing?_JSONToModelKeyBlock(value):_modelToJSONKeyBlock(value); +} + ++(instancetype)mapperFromUnderscoreCaseToCamelCase +{ + JSONModelKeyMapBlock toModel = ^ NSString* (NSString* keyName) { + + //bail early if no transformation required + if ([keyName rangeOfString:@"_"].location==NSNotFound) return keyName; + + //derive camel case out of underscore case + NSString* camelCase = [keyName capitalizedString]; + camelCase = [camelCase stringByReplacingOccurrencesOfString:@"_" withString:@""]; + camelCase = [camelCase stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[camelCase substringToIndex:1] lowercaseString] ]; + + return camelCase; + }; + + JSONModelKeyMapBlock toJSON = ^ NSString* (NSString* keyName) { + + NSMutableString* result = [NSMutableString stringWithString:keyName]; + NSRange upperCharRange = [result rangeOfCharacterFromSet:[NSCharacterSet uppercaseLetterCharacterSet]]; + + //handle upper case chars + while ( upperCharRange.location!=NSNotFound) { + + NSString* lowerChar = [[result substringWithRange:upperCharRange] lowercaseString]; + [result replaceCharactersInRange:upperCharRange + withString:[NSString stringWithFormat:@"_%@", lowerChar]]; + upperCharRange = [result rangeOfCharacterFromSet:[NSCharacterSet uppercaseLetterCharacterSet]]; + } + + //handle numbers + NSRange digitsRange = [result rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]]; + while ( digitsRange.location!=NSNotFound) { + + NSRange digitsRangeEnd = [result rangeOfString:@"\\D" options:NSRegularExpressionSearch range:NSMakeRange(digitsRange.location, result.length-digitsRange.location)]; + if (digitsRangeEnd.location == NSNotFound) { + //spands till the end of the key name + digitsRangeEnd = NSMakeRange(result.length, 1); + } + + NSRange replaceRange = NSMakeRange(digitsRange.location, digitsRangeEnd.location - digitsRange.location); + NSString* digits = [result substringWithRange:replaceRange]; + + [result replaceCharactersInRange:replaceRange withString:[NSString stringWithFormat:@"_%@", digits]]; + digitsRange = [result rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet] options:kNilOptions range:NSMakeRange(digitsRangeEnd.location+1, result.length-digitsRangeEnd.location-1)]; + } + + return result; + }; + + return [[self alloc] initWithJSONToModelBlock:toModel + modelToJSONBlock:toJSON]; + +} + ++(instancetype)mapperFromUpperCaseToLowerCase +{ + JSONModelKeyMapBlock toModel = ^ NSString* (NSString* keyName) { + NSString*lowercaseString = [keyName lowercaseString]; + return lowercaseString; + }; + + JSONModelKeyMapBlock toJSON = ^ NSString* (NSString* keyName) { + + NSString *uppercaseString = [keyName uppercaseString]; + + return uppercaseString; + }; + + return [[self alloc] initWithJSONToModelBlock:toModel + modelToJSONBlock:toJSON]; + +} + ++ (instancetype)mapper:(JSONKeyMapper *)baseKeyMapper withExceptions:(NSDictionary *)exceptions +{ + NSArray *keys = exceptions.allKeys; + NSArray *values = [exceptions objectsForKeys:keys notFoundMarker:[NSNull null]]; + + NSDictionary *toModelMap = [NSDictionary dictionaryWithObjects:values forKeys:keys]; + NSDictionary *toJsonMap = [NSDictionary dictionaryWithObjects:keys forKeys:values]; + + JSONModelKeyMapBlock toModel = ^NSString *(NSString *keyName) { + if (!keyName) + return nil; + + if (toModelMap[keyName]) + return toModelMap[keyName]; + + return baseKeyMapper.JSONToModelKeyBlock(keyName); + }; + + JSONModelKeyMapBlock toJson = ^NSString *(NSString *keyName) { + if (!keyName) + return nil; + + if (toJsonMap[keyName]) + return toJsonMap[keyName]; + + return baseKeyMapper.modelToJSONKeyBlock(keyName); + }; + + return [[self alloc] initWithJSONToModelBlock:toModel modelToJSONBlock:toJson]; +} + +@end diff --git a/JSONModel/JSONModelTransformations/JSONValueTransformer.h b/JSONModel/JSONModelTransformations/JSONValueTransformer.h new file mode 100644 index 0000000..71728a9 --- /dev/null +++ b/JSONModel/JSONModelTransformations/JSONValueTransformer.h @@ -0,0 +1,230 @@ +// +// JSONValueTransformer.h +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import +#import "JSONModelArray.h" + +///////////////////////////////////////////////////////////////////////////////////////////// + +#pragma mark - extern definitions +/** + * Boolean function to check for null values. Handy when you need to both check + * for nil and [NSNUll null] + */ +extern BOOL isNull(id value); + +///////////////////////////////////////////////////////////////////////////////////////////// + +#pragma mark - JSONValueTransformer interface +/** + * **You don't need to call methods of this class manually.** + * + * Class providing methods to transform values from one class to another. + * You are given a number of built-in transformers, but you are encouraged to + * extend this class with your own categories to add further value transformers. + * Just few examples of what can you add to JSONValueTransformer: hex colors in JSON to UIColor, + * hex numbers in JSON to NSNumber model properties, base64 encoded strings in JSON to UIImage properties, and more. + * + * The class is invoked by JSONModel while transforming incoming + * JSON types into your target class property classes, and vice versa. + * One static copy is create and store in the JSONModel class scope. + */ +@interface JSONValueTransformer : NSObject + +@property (strong, nonatomic, readonly) NSDictionary* primitivesNames; + +/** @name Resolving cluster class names */ +/** + * This method returns the umbrella class for any standard class cluster members. + * For example returns NSString when given as input NSString, NSMutableString, __CFString and __CFConstantString + * The method currently looksup a pre-defined list. + * @param sourceClass the class to get the umbrella class for + * @return Class + */ ++(Class)classByResolvingClusterClasses:(Class)sourceClass; + +#pragma mark - NSMutableString <-> NSString +/** @name Transforming to Mutable copies */ +/** + * Transforms a string value to a mutable string value + * @param string incoming string + * @return mutable string + */ +-(NSMutableString*)NSMutableStringFromNSString:(NSString*)string; + +#pragma mark - NSMutableArray <-> NSArray +/** + * Transforms an array to a mutable array + * @param array incoming array + * @return mutable array + */ +-(NSMutableArray*)NSMutableArrayFromNSArray:(NSArray*)array; + +#pragma mark - NS(Mutable)Array <- JSONModelArray +/** + * Transforms an array to a JSONModelArray + * @param array incoming array + * @return JSONModelArray + */ +-(NSArray*)NSArrayFromJSONModelArray:(JSONModelArray*)array; +-(NSMutableArray*)NSMutableArrayFromJSONModelArray:(JSONModelArray*)array; + +#pragma mark - NSMutableDictionary <-> NSDictionary +/** + * Transforms a dictionary to a mutable dictionary + * @param dict incoming dictionary + * @return mutable dictionary + */ +-(NSMutableDictionary*)NSMutableDictionaryFromNSDictionary:(NSDictionary*)dict; + +#pragma mark - NSSet <-> NSArray +/** @name Transforming Sets */ +/** + * Transforms an array to a set + * @param array incoming array + * @return set with the array's elements + */ +-(NSSet*)NSSetFromNSArray:(NSArray*)array; + +/** + * Transforms an array to a mutable set + * @param array incoming array + * @return mutable set with the array's elements + */ +-(NSMutableSet*)NSMutableSetFromNSArray:(NSArray*)array; + +/** + * Transforms a set to an array + * @param set incoming set + * @return an array with the set's elements + */ +-(NSArray*)JSONObjectFromNSSet:(NSSet*)set; + +/** + * Transforms a mutable set to an array + * @param set incoming mutable set + * @return an array with the set's elements + */ +-(NSArray*)JSONObjectFromNSMutableSet:(NSMutableSet*)set; + +#pragma mark - BOOL <-> number/string +/** @name Transforming JSON types */ +/** + * Transforms a number object to a bool number object + * @param number the number to convert + * @return the resulting number + */ +-(NSNumber*)BOOLFromNSNumber:(NSNumber*)number; + +/** + * Transforms a number object to a bool number object + * @param string the string value to convert, "0" converts to NO, everything else to YES + * @return the resulting number + */ +-(NSNumber*)BOOLFromNSString:(NSString*)string; + +/** + * Transforms a BOOL value to a bool number object + * @param number an NSNumber value coming from the model + * @return the result number + */ +-(NSNumber*)JSONObjectFromBOOL:(NSNumber*)number; + +#pragma mark - string <-> number +/** + * Transforms a string object to a number object + * @param string the string to convert + * @return the resulting number + */ +-(NSNumber*)NSNumberFromNSString:(NSString*)string; + +/** + * Transforms a number object to a string object + * @param number the number to convert + * @return the resulting string + */ +-(NSString*)NSStringFromNSNumber:(NSNumber*)number; + +/** + * Transforms a string object to a nsdecimalnumber object + * @param string the string to convert + * @return the resulting number + */ +-(NSDecimalNumber*)NSDecimalNumberFromNSString:(NSString*)string; + +/** + * Transforms a nsdecimalnumber object to a string object + * @param number the number to convert + * @return the resulting string + */ +-(NSString*)NSStringFromNSDecimalNumber:(NSDecimalNumber*)number; + + +#pragma mark - string <-> url +/** @name Transforming URLs */ +/** + * Transforms a string object to an NSURL object + * @param string the string to convert + * @return the resulting url object + */ +-(NSURL*)NSURLFromNSString:(NSString*)string; + +/** + * Transforms an NSURL object to a string + * @param url the url object to convert + * @return the resulting string + */ +-(NSString*)JSONObjectFromNSURL:(NSURL*)url; + +#pragma mark - string <-> time zone + +/** @name Transforming NSTimeZone */ +/** + * Transforms a string object to an NSTimeZone object + * @param string the string to convert + * @return the resulting NSTimeZone object + */ +- (NSTimeZone *)NSTimeZoneFromNSString:(NSString*)string; + +/** + * Transforms an NSTimeZone object to a string + * @param timeZone the time zone object to convert + * @return the resulting string + */ +- (NSString *)JSONObjectFromNSTimeZone:(NSTimeZone *)timeZone; + +#pragma mark - string <-> date +/** @name Transforming Dates */ +/** + * The following two methods are not public. This way if there is a category on converting + * dates it'll override them. If there isn't a category the default methods found in the .m + * file will be invoked. If these are public a warning is produced at the point of overriding + * them in a category, so they have to stay hidden here. + */ + +//-(NSDate*)NSDateFromNSString:(NSString*)string; +//-(NSString*)JSONObjectFromNSDate:(NSDate*)date; + +#pragma mark - number <-> date + +/** + * Transforms a number to an NSDate object + * @param number the number to convert + * @return the resulting date + */ +- (NSDate*)NSDateFromNSNumber:(NSNumber*)number; + +@end diff --git a/JSONModel/JSONModelTransformations/JSONValueTransformer.m b/JSONModel/JSONModelTransformations/JSONValueTransformer.m new file mode 100644 index 0000000..c902fdf --- /dev/null +++ b/JSONModel/JSONModelTransformations/JSONValueTransformer.m @@ -0,0 +1,274 @@ +// +// JSONValueTransformer.m +// +// @version 1.2 +// @author Marin Todorov (http://www.underplot.com) and contributors +// + +// Copyright (c) 2012-2015 Marin Todorov, Underplot ltd. +// This code is distributed under the terms and conditions of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +#import "JSONValueTransformer.h" +#import "JSONModelArray.h" + +#pragma mark - functions +extern BOOL isNull(id value) +{ + if (!value) return YES; + if ([value isKindOfClass:[NSNull class]]) return YES; + + return NO; +} + +@implementation JSONValueTransformer + +-(id)init +{ + self = [super init]; + if (self) { + _primitivesNames = @{@"f":@"float", @"i":@"int", @"d":@"double", @"l":@"long", @"c":@"BOOL", @"s":@"short", @"q":@"long", + //and some famous aliases of primitive types + // BOOL is now "B" on iOS __LP64 builds + @"I":@"NSInteger", @"Q":@"NSUInteger", @"B":@"BOOL", + + @"@?":@"Block"}; + } + return self; +} + ++(Class)classByResolvingClusterClasses:(Class)sourceClass +{ + //check for all variations of strings + if ([sourceClass isSubclassOfClass:[NSString class]]) { + return [NSString class]; + } + + //check for all variations of numbers + if ([sourceClass isSubclassOfClass:[NSNumber class]]) { + return [NSNumber class]; + } + + //check for all variations of dictionaries + if ([sourceClass isSubclassOfClass:[NSArray class]]) { + return [NSArray class]; + } + + //check for all variations of arrays + if ([sourceClass isSubclassOfClass:[NSDictionary class]]) { + return [NSDictionary class]; + } + + //check for all variations of dates + if ([sourceClass isSubclassOfClass:[NSDate class]]) { + return [NSDate class]; + } + + //no cluster parent class found + return sourceClass; +} + +#pragma mark - NSMutableString <-> NSString +-(NSMutableString*)NSMutableStringFromNSString:(NSString*)string +{ + return [NSMutableString stringWithString:string]; +} + +#pragma mark - NSMutableArray <-> NSArray +-(NSMutableArray*)NSMutableArrayFromNSArray:(NSArray*)array +{ + if ([array isKindOfClass:[JSONModelArray class]]) { + //it's a jsonmodelarray already, just return it + return (id)array; + } + + return [NSMutableArray arrayWithArray:array]; +} + +#pragma mark - NS(Mutable)Array <- JSONModelArray +-(NSArray*)NSArrayFromJSONModelArray:(JSONModelArray*)array +{ + return (NSMutableArray*)array; +} + +-(NSMutableArray*)NSMutableArrayFromJSONModelArray:(JSONModelArray*)array +{ + return (NSMutableArray*)array; +} + + +#pragma mark - NSMutableDictionary <-> NSDictionary +-(NSMutableDictionary*)NSMutableDictionaryFromNSDictionary:(NSDictionary*)dict +{ + return [NSMutableDictionary dictionaryWithDictionary:dict]; +} + +#pragma mark - NSSet <-> NSArray +-(NSSet*)NSSetFromNSArray:(NSArray*)array +{ + return [NSSet setWithArray:array]; +} + +-(NSMutableSet*)NSMutableSetFromNSArray:(NSArray*)array +{ + return [NSMutableSet setWithArray:array]; +} + +-(id)JSONObjectFromNSSet:(NSSet*)set +{ + return [set allObjects]; +} + +-(id)JSONObjectFromNSMutableSet:(NSMutableSet*)set +{ + return [set allObjects]; +} + +// +// 0 converts to NO, everything else converts to YES +// + +#pragma mark - BOOL <-> number/string +-(NSNumber*)BOOLFromNSNumber:(NSNumber*)number +{ + if (isNull(number)) return [NSNumber numberWithBool:NO]; + return [NSNumber numberWithBool: number.intValue==0?NO:YES]; +} + +-(NSNumber*)BOOLFromNSString:(NSString*)string +{ + if (string != nil && + ([string caseInsensitiveCompare:@"true"] == NSOrderedSame || + [string caseInsensitiveCompare:@"yes"] == NSOrderedSame)) { + return [NSNumber numberWithBool:YES]; + } + return [NSNumber numberWithBool: ([string intValue]==0)?NO:YES]; +} + +-(NSNumber*)JSONObjectFromBOOL:(NSNumber*)number +{ + return [NSNumber numberWithBool: number.intValue==0?NO:YES]; +} + +#pragma mark - string/number <-> float +-(float)floatFromObject:(id)obj +{ + return [obj floatValue]; +} + +-(float)floatFromNSString:(NSString*)string +{ + return [self floatFromObject:string]; +} + +-(float)floatFromNSNumber:(NSNumber*)number +{ + return [self floatFromObject:number]; +} + +-(NSNumber*)NSNumberFromfloat:(float)f +{ + return [NSNumber numberWithFloat:f]; +} + +#pragma mark - string <-> number +-(NSNumber*)NSNumberFromNSString:(NSString*)string +{ + return [NSNumber numberWithDouble:[string doubleValue]]; +} + +-(NSString*)NSStringFromNSNumber:(NSNumber*)number +{ + return [number stringValue]; +} + +-(NSDecimalNumber*)NSDecimalNumberFromNSString:(NSString*)string +{ + return [NSDecimalNumber decimalNumberWithString:string]; +} + +-(NSString*)NSStringFromNSDecimalNumber:(NSDecimalNumber*)number +{ + return [number stringValue]; +} + +#pragma mark - string <-> url +-(NSURL*)NSURLFromNSString:(NSString*)string +{ + // do not change this behavior - there are other ways of overriding it + // see: https://github.com/icanzilb/JSONModel/pull/119 + return [NSURL URLWithString:string]; +} + +-(NSString*)JSONObjectFromNSURL:(NSURL*)url +{ + return [url absoluteString]; +} + +#pragma mark - string <-> date +-(NSDateFormatter*)importDateFormatter +{ + static dispatch_once_t onceInput; + static NSDateFormatter* inputDateFormatter; + dispatch_once(&onceInput, ^{ + inputDateFormatter = [[NSDateFormatter alloc] init]; + [inputDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [inputDateFormatter setDateFormat:@"yyyy-MM-dd'T'HHmmssZZZ"]; + }); + return inputDateFormatter; +} + +-(NSDate*)__NSDateFromNSString:(NSString*)string +{ + string = [string stringByReplacingOccurrencesOfString:@":" withString:@""]; // this is such an ugly code, is this the only way? + return [self.importDateFormatter dateFromString: string]; +} + +-(NSString*)__JSONObjectFromNSDate:(NSDate*)date +{ + static dispatch_once_t onceOutput; + static NSDateFormatter *outputDateFormatter; + dispatch_once(&onceOutput, ^{ + outputDateFormatter = [[NSDateFormatter alloc] init]; + [outputDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [outputDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZ"]; + }); + return [outputDateFormatter stringFromDate:date]; +} + +#pragma mark - number <-> date +- (NSDate*)NSDateFromNSNumber:(NSNumber*)number +{ + return [NSDate dateWithTimeIntervalSince1970:number.doubleValue]; +} + +#pragma mark - string <-> NSTimeZone + +- (NSTimeZone *)NSTimeZoneFromNSString:(NSString *)string { + return [NSTimeZone timeZoneWithName:string]; +} + +- (id)JSONObjectFromNSTimeZone:(NSTimeZone *)timeZone { + return [timeZone name]; +} + +#pragma mark - hidden transform for empty dictionaries +//https://github.com/icanzilb/JSONModel/issues/163 +-(NSDictionary*)__NSDictionaryFromNSArray:(NSArray*)array +{ + if (array.count==0) return @{}; + return (id)array; +} + +-(NSMutableDictionary*)__NSMutableDictionaryFromNSArray:(NSArray*)array +{ + if (array.count==0) return [[self __NSDictionaryFromNSArray:array] mutableCopy]; + return (id)array; +} + +@end