├── .gitignore ├── Example ├── TLFormView.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── TLFormView │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ ├── ViewController.h │ ├── ViewController.m │ └── main.m ├── LICENSE ├── Pod ├── Assets │ └── .gitkeep └── Classes │ ├── .gitkeep │ ├── Extensions │ ├── NSNumber+NumberType.h │ └── NSNumber+NumberType.m │ ├── Fields │ ├── TLFormAllFields.h │ ├── TLFormField+Protected.h │ ├── TLFormField.h │ ├── TLFormField.m │ ├── TLFormFieldDateTime.h │ ├── TLFormFieldDateTime.m │ ├── TLFormFieldImage.h │ ├── TLFormFieldImage.m │ ├── TLFormFieldList.h │ ├── TLFormFieldList.m │ ├── TLFormFieldMultiLine.h │ ├── TLFormFieldMultiLine.m │ ├── TLFormFieldNumeric.h │ ├── TLFormFieldNumeric.m │ ├── TLFormFieldSelect.h │ ├── TLFormFieldSelect.m │ ├── TLFormFieldSingleLine.h │ ├── TLFormFieldSingleLine.m │ ├── TLFormFieldTitle.h │ ├── TLFormFieldTitle.m │ ├── TLFormFieldYesNo.h │ └── TLFormFieldYesNo.m │ ├── Style │ ├── TLFormField+UIAppearance.h │ └── TLFormField+UIAppearance.m │ ├── TLFormModel.h │ ├── TLFormModel.m │ ├── TLFormView.h │ └── TLFormView.m ├── README.md ├── Screenshots ├── in-place_help.png ├── ipad_ex_1.png ├── iphone_ex_1.png └── iphone_quick_ex.png └── TLFormView.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Note: if you ignore the Pods directory, make sure to uncomment 30 | # `pod install` in .travis.yml 31 | # 32 | # Pods/ 33 | -------------------------------------------------------------------------------- /Example/TLFormView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B5B54FF1AC4F0D900604DE2 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B5B54FE1AC4F0D900604DE2 /* MobileCoreServices.framework */; }; 11 | 9BE9B0EE1B02562E009DA0BD /* TLFormFieldDateTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE9B0ED1B02562E009DA0BD /* TLFormFieldDateTime.m */; }; 12 | 9BE9B0F11B03A81B009DA0BD /* TLFormFieldNumeric.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE9B0F01B03A81B009DA0BD /* TLFormFieldNumeric.m */; }; 13 | 9BE9B0F51B03BD13009DA0BD /* NSNumber+NumberType.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE9B0F41B03BD13009DA0BD /* NSNumber+NumberType.m */; }; 14 | 9BE9B0F81B03C24A009DA0BD /* TLFormFieldYesNo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE9B0F71B03C24A009DA0BD /* TLFormFieldYesNo.m */; }; 15 | 9BE9B0FB1B03D257009DA0BD /* TLFormFieldSelect.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE9B0FA1B03D257009DA0BD /* TLFormFieldSelect.m */; }; 16 | 9BECE1881A9B5B4D00CE2FD5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1871A9B5B4D00CE2FD5 /* main.m */; }; 17 | 9BECE18B1A9B5B4D00CE2FD5 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE18A1A9B5B4D00CE2FD5 /* AppDelegate.m */; }; 18 | 9BECE18E1A9B5B4D00CE2FD5 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE18D1A9B5B4D00CE2FD5 /* ViewController.m */; }; 19 | 9BECE1911A9B5B4D00CE2FD5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BECE18F1A9B5B4D00CE2FD5 /* Main.storyboard */; }; 20 | 9BECE1931A9B5B4D00CE2FD5 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9BECE1921A9B5B4D00CE2FD5 /* Images.xcassets */; }; 21 | 9BECE1961A9B5B4D00CE2FD5 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9BECE1941A9B5B4D00CE2FD5 /* LaunchScreen.xib */; }; 22 | 9BECE1B01A9B5B9800CE2FD5 /* TLFormView.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1AE1A9B5B9800CE2FD5 /* TLFormView.m */; }; 23 | 9BECE1B41A9CAD7800CE2FD5 /* TLFormField.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1B31A9CAD7800CE2FD5 /* TLFormField.m */; }; 24 | 9BECE1B81A9CB6DD00CE2FD5 /* TLFormFieldList.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1B71A9CB6DD00CE2FD5 /* TLFormFieldList.m */; }; 25 | 9BECE1BB1A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1BA1A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.m */; }; 26 | 9BECE1BE1A9CBAEC00CE2FD5 /* TLFormFieldSingleLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1BD1A9CBAEB00CE2FD5 /* TLFormFieldSingleLine.m */; }; 27 | 9BECE1C11A9CBB8B00CE2FD5 /* TLFormFieldTitle.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1C01A9CBB8B00CE2FD5 /* TLFormFieldTitle.m */; }; 28 | 9BECE1C41A9CBBD100CE2FD5 /* TLFormFieldImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1C31A9CBBD100CE2FD5 /* TLFormFieldImage.m */; }; 29 | 9BECE1CA1A9CC4FE00CE2FD5 /* TLFormModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1C91A9CC4FE00CE2FD5 /* TLFormModel.m */; }; 30 | 9BECE1D21A9F899500CE2FD5 /* TLFormField+UIAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE1D11A9F899500CE2FD5 /* TLFormField+UIAppearance.m */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 9B5B54FE1AC4F0D900604DE2 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 35 | 9BE9B0EC1B02562E009DA0BD /* TLFormFieldDateTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldDateTime.h; sourceTree = ""; }; 36 | 9BE9B0ED1B02562E009DA0BD /* TLFormFieldDateTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLFormFieldDateTime.m; sourceTree = ""; }; 37 | 9BE9B0EF1B03A81B009DA0BD /* TLFormFieldNumeric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldNumeric.h; sourceTree = ""; }; 38 | 9BE9B0F01B03A81B009DA0BD /* TLFormFieldNumeric.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLFormFieldNumeric.m; sourceTree = ""; }; 39 | 9BE9B0F31B03BD13009DA0BD /* NSNumber+NumberType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNumber+NumberType.h"; sourceTree = ""; }; 40 | 9BE9B0F41B03BD13009DA0BD /* NSNumber+NumberType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNumber+NumberType.m"; sourceTree = ""; }; 41 | 9BE9B0F61B03C24A009DA0BD /* TLFormFieldYesNo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldYesNo.h; sourceTree = ""; }; 42 | 9BE9B0F71B03C24A009DA0BD /* TLFormFieldYesNo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLFormFieldYesNo.m; sourceTree = ""; }; 43 | 9BE9B0F91B03D257009DA0BD /* TLFormFieldSelect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldSelect.h; sourceTree = ""; }; 44 | 9BE9B0FA1B03D257009DA0BD /* TLFormFieldSelect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLFormFieldSelect.m; sourceTree = ""; }; 45 | 9BECE1821A9B5B4D00CE2FD5 /* TLFormView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TLFormView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 9BECE1861A9B5B4D00CE2FD5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 9BECE1871A9B5B4D00CE2FD5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 48 | 9BECE1891A9B5B4D00CE2FD5 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 49 | 9BECE18A1A9B5B4D00CE2FD5 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 50 | 9BECE18C1A9B5B4D00CE2FD5 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 51 | 9BECE18D1A9B5B4D00CE2FD5 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 52 | 9BECE1901A9B5B4D00CE2FD5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 53 | 9BECE1921A9B5B4D00CE2FD5 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 54 | 9BECE1951A9B5B4D00CE2FD5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 55 | 9BECE1AD1A9B5B9800CE2FD5 /* TLFormView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = TLFormView.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 56 | 9BECE1AE1A9B5B9800CE2FD5 /* TLFormView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormView.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 57 | 9BECE1B21A9CAD7800CE2FD5 /* TLFormField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormField.h; sourceTree = ""; }; 58 | 9BECE1B31A9CAD7800CE2FD5 /* TLFormField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormField.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 59 | 9BECE1B51A9CB4F200CE2FD5 /* TLFormField+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TLFormField+Protected.h"; sourceTree = ""; }; 60 | 9BECE1B61A9CB6DD00CE2FD5 /* TLFormFieldList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldList.h; sourceTree = ""; }; 61 | 9BECE1B71A9CB6DD00CE2FD5 /* TLFormFieldList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormFieldList.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 62 | 9BECE1B91A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = TLFormFieldMultiLine.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 63 | 9BECE1BA1A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormFieldMultiLine.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 64 | 9BECE1BC1A9CBAEB00CE2FD5 /* TLFormFieldSingleLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldSingleLine.h; sourceTree = ""; }; 65 | 9BECE1BD1A9CBAEB00CE2FD5 /* TLFormFieldSingleLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormFieldSingleLine.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 66 | 9BECE1BF1A9CBB8B00CE2FD5 /* TLFormFieldTitle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldTitle.h; sourceTree = ""; }; 67 | 9BECE1C01A9CBB8B00CE2FD5 /* TLFormFieldTitle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormFieldTitle.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 68 | 9BECE1C21A9CBBD100CE2FD5 /* TLFormFieldImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormFieldImage.h; sourceTree = ""; }; 69 | 9BECE1C31A9CBBD100CE2FD5 /* TLFormFieldImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormFieldImage.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 70 | 9BECE1C81A9CC4FE00CE2FD5 /* TLFormModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLFormModel.h; sourceTree = ""; }; 71 | 9BECE1C91A9CC4FE00CE2FD5 /* TLFormModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = TLFormModel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 72 | 9BECE1D01A9F899500CE2FD5 /* TLFormField+UIAppearance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TLFormField+UIAppearance.h"; sourceTree = ""; }; 73 | 9BECE1D11A9F899500CE2FD5 /* TLFormField+UIAppearance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TLFormField+UIAppearance.m"; sourceTree = ""; }; 74 | 9BECE1D31A9F89E200CE2FD5 /* TLFormAllFields.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLFormAllFields.h; sourceTree = ""; }; 75 | /* End PBXFileReference section */ 76 | 77 | /* Begin PBXFrameworksBuildPhase section */ 78 | 9BECE17F1A9B5B4D00CE2FD5 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | 9B5B54FF1AC4F0D900604DE2 /* MobileCoreServices.framework in Frameworks */, 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | /* End PBXFrameworksBuildPhase section */ 87 | 88 | /* Begin PBXGroup section */ 89 | 9BE9B0F21B03BCF3009DA0BD /* Extensions */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 9BE9B0F31B03BD13009DA0BD /* NSNumber+NumberType.h */, 93 | 9BE9B0F41B03BD13009DA0BD /* NSNumber+NumberType.m */, 94 | ); 95 | path = Extensions; 96 | sourceTree = ""; 97 | }; 98 | 9BECE1791A9B5B4D00CE2FD5 = { 99 | isa = PBXGroup; 100 | children = ( 101 | 9B5B54FE1AC4F0D900604DE2 /* MobileCoreServices.framework */, 102 | 9BECE1841A9B5B4D00CE2FD5 /* TLFormView */, 103 | 9BECE1831A9B5B4D00CE2FD5 /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | 9BECE1831A9B5B4D00CE2FD5 /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 9BECE1821A9B5B4D00CE2FD5 /* TLFormView.app */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | 9BECE1841A9B5B4D00CE2FD5 /* TLFormView */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 9BECE1AB1A9B5B9800CE2FD5 /* Classes */, 119 | 9BECE1891A9B5B4D00CE2FD5 /* AppDelegate.h */, 120 | 9BECE18A1A9B5B4D00CE2FD5 /* AppDelegate.m */, 121 | 9BECE18C1A9B5B4D00CE2FD5 /* ViewController.h */, 122 | 9BECE18D1A9B5B4D00CE2FD5 /* ViewController.m */, 123 | 9BECE18F1A9B5B4D00CE2FD5 /* Main.storyboard */, 124 | 9BECE1921A9B5B4D00CE2FD5 /* Images.xcassets */, 125 | 9BECE1941A9B5B4D00CE2FD5 /* LaunchScreen.xib */, 126 | 9BECE1851A9B5B4D00CE2FD5 /* Supporting Files */, 127 | ); 128 | path = TLFormView; 129 | sourceTree = ""; 130 | }; 131 | 9BECE1851A9B5B4D00CE2FD5 /* Supporting Files */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 9BECE1861A9B5B4D00CE2FD5 /* Info.plist */, 135 | 9BECE1871A9B5B4D00CE2FD5 /* main.m */, 136 | ); 137 | name = "Supporting Files"; 138 | sourceTree = ""; 139 | }; 140 | 9BECE1AB1A9B5B9800CE2FD5 /* Classes */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 9BE9B0F21B03BCF3009DA0BD /* Extensions */, 144 | 9BECE1CF1A9F898900CE2FD5 /* Style */, 145 | 9BECE1B11A9CACEA00CE2FD5 /* Fields */, 146 | 9BECE1AD1A9B5B9800CE2FD5 /* TLFormView.h */, 147 | 9BECE1AE1A9B5B9800CE2FD5 /* TLFormView.m */, 148 | 9BECE1C81A9CC4FE00CE2FD5 /* TLFormModel.h */, 149 | 9BECE1C91A9CC4FE00CE2FD5 /* TLFormModel.m */, 150 | ); 151 | name = Classes; 152 | path = ../../Pod/Classes; 153 | sourceTree = ""; 154 | }; 155 | 9BECE1B11A9CACEA00CE2FD5 /* Fields */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 9BECE1B21A9CAD7800CE2FD5 /* TLFormField.h */, 159 | 9BECE1B31A9CAD7800CE2FD5 /* TLFormField.m */, 160 | 9BECE1B51A9CB4F200CE2FD5 /* TLFormField+Protected.h */, 161 | 9BECE1B61A9CB6DD00CE2FD5 /* TLFormFieldList.h */, 162 | 9BECE1B71A9CB6DD00CE2FD5 /* TLFormFieldList.m */, 163 | 9BECE1B91A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.h */, 164 | 9BECE1BA1A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.m */, 165 | 9BECE1BC1A9CBAEB00CE2FD5 /* TLFormFieldSingleLine.h */, 166 | 9BECE1BD1A9CBAEB00CE2FD5 /* TLFormFieldSingleLine.m */, 167 | 9BECE1BF1A9CBB8B00CE2FD5 /* TLFormFieldTitle.h */, 168 | 9BECE1C01A9CBB8B00CE2FD5 /* TLFormFieldTitle.m */, 169 | 9BECE1C21A9CBBD100CE2FD5 /* TLFormFieldImage.h */, 170 | 9BECE1C31A9CBBD100CE2FD5 /* TLFormFieldImage.m */, 171 | 9BECE1D31A9F89E200CE2FD5 /* TLFormAllFields.h */, 172 | 9BE9B0EC1B02562E009DA0BD /* TLFormFieldDateTime.h */, 173 | 9BE9B0ED1B02562E009DA0BD /* TLFormFieldDateTime.m */, 174 | 9BE9B0EF1B03A81B009DA0BD /* TLFormFieldNumeric.h */, 175 | 9BE9B0F01B03A81B009DA0BD /* TLFormFieldNumeric.m */, 176 | 9BE9B0F61B03C24A009DA0BD /* TLFormFieldYesNo.h */, 177 | 9BE9B0F71B03C24A009DA0BD /* TLFormFieldYesNo.m */, 178 | 9BE9B0F91B03D257009DA0BD /* TLFormFieldSelect.h */, 179 | 9BE9B0FA1B03D257009DA0BD /* TLFormFieldSelect.m */, 180 | ); 181 | path = Fields; 182 | sourceTree = ""; 183 | }; 184 | 9BECE1CF1A9F898900CE2FD5 /* Style */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 9BECE1D01A9F899500CE2FD5 /* TLFormField+UIAppearance.h */, 188 | 9BECE1D11A9F899500CE2FD5 /* TLFormField+UIAppearance.m */, 189 | ); 190 | path = Style; 191 | sourceTree = ""; 192 | }; 193 | /* End PBXGroup section */ 194 | 195 | /* Begin PBXNativeTarget section */ 196 | 9BECE1811A9B5B4D00CE2FD5 /* TLFormView */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = 9BECE1A51A9B5B4E00CE2FD5 /* Build configuration list for PBXNativeTarget "TLFormView" */; 199 | buildPhases = ( 200 | 9BECE17E1A9B5B4D00CE2FD5 /* Sources */, 201 | 9BECE17F1A9B5B4D00CE2FD5 /* Frameworks */, 202 | 9BECE1801A9B5B4D00CE2FD5 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | ); 208 | name = TLFormView; 209 | productName = TLFormView; 210 | productReference = 9BECE1821A9B5B4D00CE2FD5 /* TLFormView.app */; 211 | productType = "com.apple.product-type.application"; 212 | }; 213 | /* End PBXNativeTarget section */ 214 | 215 | /* Begin PBXProject section */ 216 | 9BECE17A1A9B5B4D00CE2FD5 /* Project object */ = { 217 | isa = PBXProject; 218 | attributes = { 219 | LastUpgradeCheck = 0610; 220 | ORGANIZATIONNAME = "Bruno Berisso"; 221 | TargetAttributes = { 222 | 9BECE1811A9B5B4D00CE2FD5 = { 223 | CreatedOnToolsVersion = 6.1.1; 224 | }; 225 | }; 226 | }; 227 | buildConfigurationList = 9BECE17D1A9B5B4D00CE2FD5 /* Build configuration list for PBXProject "TLFormView" */; 228 | compatibilityVersion = "Xcode 3.2"; 229 | developmentRegion = English; 230 | hasScannedForEncodings = 0; 231 | knownRegions = ( 232 | en, 233 | Base, 234 | ); 235 | mainGroup = 9BECE1791A9B5B4D00CE2FD5; 236 | productRefGroup = 9BECE1831A9B5B4D00CE2FD5 /* Products */; 237 | projectDirPath = ""; 238 | projectRoot = ""; 239 | targets = ( 240 | 9BECE1811A9B5B4D00CE2FD5 /* TLFormView */, 241 | ); 242 | }; 243 | /* End PBXProject section */ 244 | 245 | /* Begin PBXResourcesBuildPhase section */ 246 | 9BECE1801A9B5B4D00CE2FD5 /* Resources */ = { 247 | isa = PBXResourcesBuildPhase; 248 | buildActionMask = 2147483647; 249 | files = ( 250 | 9BECE1911A9B5B4D00CE2FD5 /* Main.storyboard in Resources */, 251 | 9BECE1961A9B5B4D00CE2FD5 /* LaunchScreen.xib in Resources */, 252 | 9BECE1931A9B5B4D00CE2FD5 /* Images.xcassets in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 9BECE17E1A9B5B4D00CE2FD5 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 9BECE18E1A9B5B4D00CE2FD5 /* ViewController.m in Sources */, 264 | 9BE9B0FB1B03D257009DA0BD /* TLFormFieldSelect.m in Sources */, 265 | 9BECE1D21A9F899500CE2FD5 /* TLFormField+UIAppearance.m in Sources */, 266 | 9BE9B0F11B03A81B009DA0BD /* TLFormFieldNumeric.m in Sources */, 267 | 9BE9B0F81B03C24A009DA0BD /* TLFormFieldYesNo.m in Sources */, 268 | 9BECE18B1A9B5B4D00CE2FD5 /* AppDelegate.m in Sources */, 269 | 9BECE1B81A9CB6DD00CE2FD5 /* TLFormFieldList.m in Sources */, 270 | 9BECE1B01A9B5B9800CE2FD5 /* TLFormView.m in Sources */, 271 | 9BE9B0F51B03BD13009DA0BD /* NSNumber+NumberType.m in Sources */, 272 | 9BECE1C41A9CBBD100CE2FD5 /* TLFormFieldImage.m in Sources */, 273 | 9BE9B0EE1B02562E009DA0BD /* TLFormFieldDateTime.m in Sources */, 274 | 9BECE1BE1A9CBAEC00CE2FD5 /* TLFormFieldSingleLine.m in Sources */, 275 | 9BECE1881A9B5B4D00CE2FD5 /* main.m in Sources */, 276 | 9BECE1C11A9CBB8B00CE2FD5 /* TLFormFieldTitle.m in Sources */, 277 | 9BECE1CA1A9CC4FE00CE2FD5 /* TLFormModel.m in Sources */, 278 | 9BECE1B41A9CAD7800CE2FD5 /* TLFormField.m in Sources */, 279 | 9BECE1BB1A9CBA3F00CE2FD5 /* TLFormFieldMultiLine.m in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | /* End PBXSourcesBuildPhase section */ 284 | 285 | /* Begin PBXVariantGroup section */ 286 | 9BECE18F1A9B5B4D00CE2FD5 /* Main.storyboard */ = { 287 | isa = PBXVariantGroup; 288 | children = ( 289 | 9BECE1901A9B5B4D00CE2FD5 /* Base */, 290 | ); 291 | name = Main.storyboard; 292 | sourceTree = ""; 293 | }; 294 | 9BECE1941A9B5B4D00CE2FD5 /* LaunchScreen.xib */ = { 295 | isa = PBXVariantGroup; 296 | children = ( 297 | 9BECE1951A9B5B4D00CE2FD5 /* Base */, 298 | ); 299 | name = LaunchScreen.xib; 300 | sourceTree = ""; 301 | }; 302 | /* End PBXVariantGroup section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | 9BECE1A31A9B5B4E00CE2FD5 /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 310 | CLANG_CXX_LIBRARY = "libc++"; 311 | CLANG_ENABLE_MODULES = YES; 312 | CLANG_ENABLE_OBJC_ARC = YES; 313 | CLANG_WARN_BOOL_CONVERSION = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 316 | CLANG_WARN_EMPTY_BODY = YES; 317 | CLANG_WARN_ENUM_CONVERSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 320 | CLANG_WARN_UNREACHABLE_CODE = YES; 321 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 322 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 323 | COPY_PHASE_STRIP = NO; 324 | ENABLE_STRICT_OBJC_MSGSEND = YES; 325 | GCC_C_LANGUAGE_STANDARD = gnu99; 326 | GCC_DYNAMIC_NO_PIC = NO; 327 | GCC_OPTIMIZATION_LEVEL = 0; 328 | GCC_PREPROCESSOR_DEFINITIONS = ( 329 | "DEBUG=1", 330 | "$(inherited)", 331 | ); 332 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 333 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 334 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 335 | GCC_WARN_UNDECLARED_SELECTOR = YES; 336 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 337 | GCC_WARN_UNUSED_FUNCTION = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 340 | MTL_ENABLE_DEBUG_INFO = YES; 341 | ONLY_ACTIVE_ARCH = YES; 342 | SDKROOT = iphoneos; 343 | TARGETED_DEVICE_FAMILY = "1,2"; 344 | }; 345 | name = Debug; 346 | }; 347 | 9BECE1A41A9B5B4E00CE2FD5 /* Release */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ALWAYS_SEARCH_USER_PATHS = NO; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 352 | CLANG_CXX_LIBRARY = "libc++"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_WARN_BOOL_CONVERSION = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN_UNREACHABLE_CODE = YES; 363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 364 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 365 | COPY_PHASE_STRIP = YES; 366 | ENABLE_NS_ASSERTIONS = NO; 367 | ENABLE_STRICT_OBJC_MSGSEND = YES; 368 | GCC_C_LANGUAGE_STANDARD = gnu99; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | SDKROOT = iphoneos; 378 | TARGETED_DEVICE_FAMILY = "1,2"; 379 | VALIDATE_PRODUCT = YES; 380 | }; 381 | name = Release; 382 | }; 383 | 9BECE1A61A9B5B4E00CE2FD5 /* Debug */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 387 | INFOPLIST_FILE = TLFormView/Info.plist; 388 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | }; 391 | name = Debug; 392 | }; 393 | 9BECE1A71A9B5B4E00CE2FD5 /* Release */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | INFOPLIST_FILE = TLFormView/Info.plist; 398 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 399 | PRODUCT_NAME = "$(TARGET_NAME)"; 400 | }; 401 | name = Release; 402 | }; 403 | /* End XCBuildConfiguration section */ 404 | 405 | /* Begin XCConfigurationList section */ 406 | 9BECE17D1A9B5B4D00CE2FD5 /* Build configuration list for PBXProject "TLFormView" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 9BECE1A31A9B5B4E00CE2FD5 /* Debug */, 410 | 9BECE1A41A9B5B4E00CE2FD5 /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | 9BECE1A51A9B5B4E00CE2FD5 /* Build configuration list for PBXNativeTarget "TLFormView" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 9BECE1A61A9B5B4E00CE2FD5 /* Debug */, 419 | 9BECE1A71A9B5B4E00CE2FD5 /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | /* End XCConfigurationList section */ 425 | }; 426 | rootObject = 9BECE17A1A9B5B4D00CE2FD5 /* Project object */; 427 | } 428 | -------------------------------------------------------------------------------- /Example/TLFormView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/TLFormView/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/23/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /Example/TLFormView/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/23/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | - (void)applicationWillResignActive:(UIApplication *)application { 24 | // 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. 25 | // 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. 26 | } 27 | 28 | - (void)applicationDidEnterBackground:(UIApplication *)application { 29 | // 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. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | - (void)applicationWillEnterForeground:(UIApplication *)application { 34 | // 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. 35 | } 36 | 37 | - (void)applicationDidBecomeActive:(UIApplication *)application { 38 | // 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. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Example/TLFormView/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/TLFormView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Example/TLFormView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Example/TLFormView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.tryolabs.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/TLFormView/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/23/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /Example/TLFormView/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/23/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "TLFormView.h" 11 | #import "TLFormModel.h" 12 | #import "TLFormFieldDateTime.h" 13 | #import 14 | 15 | 16 | @interface UserModel : TLFormModel 17 | 18 | @property (nonatomic, strong) TLFormTitle *user_info; 19 | @property (nonatomic, strong) TLFormImage *avatar; 20 | @property (nonatomic, strong) TLFormText *name; 21 | @property (nonatomic, strong) TLFormNumber *age; 22 | @property (nonatomic, strong) TLFormBoolean *is_active; 23 | @property (nonatomic, strong) TLFormEnumerated *hobbies; 24 | @property (nonatomic, strong) TLFormDateTime *birthday; 25 | @property (nonatomic, strong) TLFormSeparator *separator; 26 | @property (nonatomic, strong) TLFormLongText *_description; 27 | @property (nonatomic, strong) TLFormList *friends; 28 | 29 | @end 30 | 31 | @implementation UserModel 32 | 33 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName { 34 | 35 | TLFormField *field = [super formView:form fieldForName:fieldName]; 36 | 37 | //Add an explanation of the field. If this property has a value a quetion mark icon is display next to the field title. 38 | if ([fieldName isEqualToString:@"is_active"]) 39 | field.helpText = @"A user is active when this value is true. Otherwise the user is not active (inactive) and this value shall be false. Only active users can have hobbies."; 40 | 41 | //The "hobbies" field will be visible only when the user "is active" 42 | else if ([fieldName isEqualToString:@"hobbies"]) 43 | field.visibilityPredicate = [NSPredicate predicateWithFormat:@"$is_active.value == YES"]; 44 | 45 | //Set the date field format 46 | else if ([fieldName isEqualToString:@"birthday"]) { 47 | TLFormFieldDateTime *birthdayField = (TLFormFieldDateTime *) field; 48 | birthdayField.dateFormat = @"MMMM dd, yyyy"; 49 | birthdayField.pickerMode = UIDatePickerModeDate; 50 | } 51 | 52 | //Set all the borders when we are running on iPad 53 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) 54 | field.borderStyle = TLFormFieldBorderAll; 55 | 56 | return field; 57 | } 58 | 59 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form { 60 | 61 | //For iPhone use the default implementation on TLFormModel (UITableView-like layout) 62 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) 63 | return [super constraintsFormatForFieldsInForm:form]; 64 | 65 | //For iPad we are going to place the personal info next to the avatar image and the description and list of friends below 66 | else { 67 | 68 | return @[ 69 | //Put the title at the top 70 | @"V:|-[user_info]", 71 | @"H:|-[user_info]-|", 72 | 73 | //Place the avatar on the top left 74 | @"V:[user_info]-[avatar(==230)]", 75 | @"H:|-[avatar]", 76 | 77 | //Now place all the first section fields to the right 78 | @"V:[user_info]-[name(>=44)]", 79 | @"H:|-[avatar]-[name]-|", 80 | 81 | @"V:[name]-[age(==name)]", 82 | @"H:|-[avatar(==420)]-[age]-|", 83 | 84 | @"V:[age]-[is_active(==name)]", 85 | @"H:|-[avatar]-[is_active]-|", 86 | 87 | @"V:[is_active]-[hobbies(==name@50)]", //Set a low priority to the height constraint so this is the one that the system will break when the field is hidden 88 | @"H:|-[avatar]-[hobbies]-|", 89 | 90 | //Add the separator 91 | @"V:[avatar]-[separator]", 92 | @"V:[hobbies]-[separator]", 93 | @"H:|-[separator]-|", 94 | 95 | //And the "description" and "firends" below 96 | @"V:[separator]-[_description]", 97 | @"H:|-[_description]-|", 98 | 99 | @"V:[_description]-[friends]-|", 100 | @"H:|-[friends]", 101 | 102 | @"V:[_description]-[birthday(>=44)]", 103 | @"H:|-[friends(==birthday)]-[birthday(==friends)]-|" 104 | ]; 105 | } 106 | } 107 | 108 | @end 109 | 110 | 111 | 112 | 113 | 114 | 115 | @interface ViewController () 116 | @property (weak, nonatomic) IBOutlet TLFormView *form; 117 | @end 118 | 119 | @implementation ViewController { 120 | UserModel *user; 121 | } 122 | 123 | - (void)viewDidLoad { 124 | [super viewDidLoad]; 125 | 126 | //Create and setup the object that describe the form. 127 | user = [[UserModel alloc] init]; 128 | [self setupFormValues]; 129 | 130 | //Set the model 131 | [self.form setFormModel:user]; 132 | 133 | //Make some visual tweaks 134 | self.form.margin = 0.0; 135 | self.form.backgroundColor = [UIColor colorWithRed:239/255.0 green:239/255.0 blue:244/255.0 alpha:1.0]; 136 | 137 | //Show the edit button 138 | self.navigationItem.rightBarButtonItem = self.editButtonItem; 139 | } 140 | 141 | - (void)setupFormValues { 142 | user.name = TLFormTextValue(@"John Doe"); 143 | user.age = TLFormNumberValue(@42); 144 | user.is_active = TLFormBooleanValue(YES); 145 | user.friends = TLFormListValue(@[@"friend 0", @"friend 1", @"friend 2", @"friend 3", @"friend 4"]); 146 | user.hobbies = TLFormEnumeratedValue(@"3D printing", @[@"3D printing", @"Amateur radio", @"Acting"]); 147 | 148 | user._description = TLFormLongTextValue(@"Michael O. Church about OOP: \"OOP tries to make software look like \"the real world\" as can be understood by an average person. (CheckingAccount extends Account extends HasBalance extends Object). The problem is that it encourages people to program before they think, and it allows software to be created that mostly works but no one knows why it does.\""); 149 | //Check about this passage here: http://www.quora.com/Was-object-oriented-programming-a-failure 150 | 151 | NSURL *url = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/custom_covers/216x146/413557246971119139_1385652535.jpg"]; 152 | user.avatar = TLFormImageValue(url); 153 | user.birthday = TLFormDateTimeValue([NSDate date]); 154 | } 155 | 156 | #pragma mark - Bar Buttons Actions 157 | 158 | - (void)setEditing:(BOOL)editing animated:(BOOL)animated { 159 | [super setEditing:editing animated:animated]; 160 | 161 | if (editing) { 162 | UIBarButtonItem *undo = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction:)]; 163 | self.navigationItem.leftBarButtonItem = undo; 164 | } else 165 | self.navigationItem.leftBarButtonItem = nil; 166 | 167 | self.form.editing = editing; 168 | [self.form setupFields]; 169 | } 170 | 171 | - (void)undoAction:(id)sender { 172 | [self setupFormValues]; 173 | [self setEditing:NO animated:YES]; 174 | } 175 | 176 | #pragma mark - TLFormViewDelegate 177 | 178 | - (void)formView:(TLFormView *)form didSelectField:(TLFormField *)field { 179 | //The field name is the same that the property name set in the UserModel class 180 | if (self.editing && [field.fieldName isEqualToString:@"avatar"]) { 181 | 182 | [self.view endEditing:YES]; 183 | 184 | UIActionSheet *popup = [[UIActionSheet alloc] initWithTitle:nil 185 | delegate:self 186 | cancelButtonTitle:@"cancel" 187 | destructiveButtonTitle:nil 188 | otherButtonTitles:@"camera", @"photo library", nil]; 189 | [popup showInView:self.view]; 190 | } 191 | } 192 | 193 | #pragma mark - UIActionSheetDelegate 194 | 195 | - (void)actionSheet:(UIActionSheet *)popup clickedButtonAtIndex:(NSInteger)buttonIndex { 196 | 197 | switch (buttonIndex) { 198 | case 0: 199 | [self performSelector:@selector(useCamera) withObject:nil afterDelay:0.1]; 200 | break; 201 | case 1: 202 | [self performSelector:@selector(useCameraRoll) withObject:nil afterDelay:0.1]; 203 | break; 204 | default: 205 | break; 206 | } 207 | } 208 | 209 | - (void)useCamera { 210 | if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { 211 | 212 | UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; 213 | imagePicker.delegate = self; 214 | imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; 215 | imagePicker.mediaTypes = @[(NSString *) kUTTypeImage]; 216 | imagePicker.allowsEditing = NO; 217 | [self presentViewController:imagePicker animated:YES completion:nil]; 218 | } 219 | } 220 | 221 | 222 | - (void)useCameraRoll { 223 | if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeSavedPhotosAlbum]) { 224 | 225 | UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; 226 | imagePicker.delegate = self; 227 | imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; 228 | imagePicker.mediaTypes = @[(NSString *) kUTTypeImage]; 229 | imagePicker.allowsEditing = NO; 230 | [self presentViewController:imagePicker animated:YES completion:nil]; 231 | } 232 | } 233 | 234 | 235 | #pragma mark - UIImagePickerControllerDelegate 236 | 237 | - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { 238 | [self dismissViewControllerAnimated:YES completion:nil]; 239 | } 240 | 241 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { 242 | [self dismissViewControllerAnimated:YES completion:nil]; 243 | 244 | NSString *mediaType = info[UIImagePickerControllerMediaType]; 245 | if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { 246 | 247 | UIImage *newImage = info[UIImagePickerControllerOriginalImage]; 248 | 249 | //If there is no ref url the image need to be saved 250 | NSURL *refUrl = info[UIImagePickerControllerReferenceURL]; 251 | if (!refUrl) 252 | UIImageWriteToSavedPhotosAlbum(newImage, nil, nil, nil); 253 | 254 | user.avatar = TLFormImageValue(newImage); 255 | [self.form reloadValues]; 256 | } 257 | } 258 | 259 | @end 260 | -------------------------------------------------------------------------------- /Example/TLFormView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/23/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 BrunoBerisso 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Pod/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Pod/Assets/.gitkeep -------------------------------------------------------------------------------- /Pod/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Pod/Classes/.gitkeep -------------------------------------------------------------------------------- /Pod/Classes/Extensions/NSNumber+NumberType.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSNumber+NumberType.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | extern const short kTLNumberBooleanType; 13 | extern const short kTLNumberNanType; 14 | 15 | //This category is used for handling the mapping between NSNumber types (numberWithBool:, numberWithFloat:, etc) and his string values. Ex: you give a "numberWithBool" as value 16 | //then the value is show as "Yes"/"No" and when the value is read you get a "numberWithBool" back. The same works for any type of number thanks to this category. 17 | @interface NSNumber (NumberType) 18 | 19 | - (CFNumberType)numberType; 20 | + (instancetype)numberOfType:(CFNumberType)type withValue:(id)value; 21 | 22 | @end 23 | 24 | -------------------------------------------------------------------------------- /Pod/Classes/Extensions/NSNumber+NumberType.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSNumber+NumberType.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "NSNumber+NumberType.h" 10 | 11 | 12 | //Add two specil values that will live side by side to the CFNumberType enum values 13 | const short kTLNumberBooleanType = -1; 14 | const short kTLNumberNanType = -2; 15 | 16 | @implementation NSNumber (NumberType) 17 | 18 | - (CFNumberType)numberType { 19 | //Get if a number is NSNumber-bool value (see: http://stackoverflow.com/questions/2518761/get-type-of-nsnumber ) 20 | if (self == (id) kCFBooleanFalse || self == (id) kCFBooleanTrue) 21 | return kTLNumberBooleanType; 22 | else 23 | return CFNumberGetType((CFNumberRef)self); 24 | } 25 | 26 | //Given a type and a value return the corresponding NSNumber. This is like use NSNumberFormatter but much better. 27 | + (instancetype)numberOfType:(CFNumberType)type withValue:(id)value { 28 | 29 | const void *numberValue = NULL; 30 | 31 | switch (type) { 32 | case kCFNumberCharType: 33 | case kCFNumberSInt8Type: { 34 | char tmp = [value charValue]; 35 | numberValue = &tmp; 36 | break; 37 | } 38 | case kCFNumberShortType: 39 | case kCFNumberSInt16Type: { 40 | short tmp = [value shortValue]; 41 | numberValue = &tmp; 42 | break; 43 | } 44 | case kCFNumberIntType: 45 | case kCFNumberSInt32Type: { 46 | int tmp = [value intValue]; 47 | numberValue = &tmp; 48 | break; 49 | } 50 | case kCFNumberLongType: { 51 | long tmp = [value longValue]; 52 | numberValue = &tmp; 53 | break; 54 | } 55 | case kCFNumberLongLongType: 56 | case kCFNumberSInt64Type: { 57 | long long tmp = [value longLongValue]; 58 | numberValue = &tmp; 59 | break; 60 | } 61 | case kCFNumberCGFloatType: 62 | case kCFNumberFloatType: 63 | case kCFNumberFloat32Type: { 64 | float tmp = [value floatValue]; 65 | numberValue = &tmp; 66 | break; 67 | } 68 | case kCFNumberDoubleType: 69 | case kCFNumberFloat64Type: { 70 | double tmp = [value doubleValue]; 71 | numberValue = &tmp; 72 | break; 73 | } 74 | case kCFNumberNSIntegerType: { 75 | NSInteger tmp = [value integerValue]; 76 | numberValue = &tmp; 77 | break; 78 | } 79 | default: 80 | numberValue = NULL; 81 | break; 82 | } 83 | 84 | return CFBridgingRelease(CFNumberCreate(kCFAllocatorDefault, type, numberValue)); 85 | } 86 | 87 | @end 88 | 89 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormAllFields.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormAllFields.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/26/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldList.h" 10 | #import "TLFormFieldMultiLine.h" 11 | #import "TLFormFieldSingleLine.h" 12 | #import "TLFormFieldNumeric.h" 13 | #import "TLFormFieldYesNo.h" 14 | #import "TLFormFieldSelect.h" 15 | #import "TLFormFieldTitle.h" 16 | #import "TLFormFieldImage.h" 17 | #import "TLFormFieldDateTime.h" -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormField+Protected.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormField+Protected.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | #import "TLFormField+UIAppearance.h" 11 | 12 | 13 | 14 | //Private delegate to pass events from the fields to the form 15 | 16 | @protocol TLFormFieldDelegate 17 | 18 | - (void)didSelectField:(TLFormField *)field; 19 | - (void)didChangeValueForField:(TLFormField *)field newValue:(id)value; 20 | 21 | @end 22 | 23 | 24 | extern int const TLFormFieldTitleLabelTag; 25 | extern int const TLFormFieldValueLabelTag; 26 | 27 | 28 | //Forward declaration of some properties and methods used for the subclases 29 | 30 | @interface TLFormField () 31 | 32 | @property (nonatomic, strong) id defautValue; 33 | @property (nonatomic, weak) id formDelegate; 34 | @property (nonatomic, readonly) NSDictionary *defaultMetrics; 35 | @property (nonatomic, strong) NSString *title; 36 | @property (nonatomic, strong, setter = setValue: , getter = getValue) id fieldValue; 37 | 38 | - (void)setupField:(BOOL)editing; 39 | - (UIView *)titleView; 40 | 41 | @end -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormField.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormField.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | typedef enum { 13 | TLFormFieldBorderNone = 0, 14 | TLFormFieldBorderTop = 1 << 0, 15 | TLFormFieldBorderRight = 1 << 1, 16 | TLFormFieldBorderBottom = 1 << 2, 17 | TLFormFieldBorderLeft = 1 << 3, 18 | TLFormFieldBorderAll = 255 19 | } TLFormFieldBorder; 20 | 21 | typedef char TLFormBorderStyleMask; 22 | 23 | 24 | //Uncoment this to color all the subviews to chack any posible layout issues 25 | //#define TLFormViewLayoutDebug 26 | 27 | 28 | /** 29 | @abstract Represent a filed in that will be show in a TLFormView 30 | @discussion This is the base class for all the fields that can be used in the TLFormView. 31 | */ 32 | @interface TLFormField : UIView 33 | 34 | ///The field name is used to identify a field in the form. Is never showed to the user. 35 | @property (nonatomic, strong) NSString *fieldName; 36 | 37 | ///If not empty show a quesion mark button next to the field title when the form is on edit mode that when taped show a popver with this text 38 | @property (nonatomic, strong) NSString *helpText; 39 | 40 | ///Mask that tells which borders should be draw 41 | @property (nonatomic, assign) TLFormBorderStyleMask borderStyle; 42 | 43 | /** 44 | @abstract Predicate used to determine the visibility of this field. 45 | @discussion If this predicate evaluates to TRUE the field es visible. The predicate is evaluated every time a field value change, by an user interaction or by calling 'refreshValues'. In the predicate context, SELF is the TLFormField object that holds the predicate and all the other fields are accesible as variables. 46 | 47 | Example: field.visibilityPredicate = [NSPredicate predicateWithFormat:@"$ingredients.value[SIZE] > 4"]; 48 | 49 | Keep in mind that the type of 'value' may change depending the TLFormFieldType and TLFormFieldInputType of the field being referenced in the expresion. 50 | 51 | IMPORTANT: to hide the fileds the form create an NSLayoutConstraint for the field height and set it to 0.0. If some other field in the layout definition is taied to this field height in any way it will also be affected. Also because this add a new constraint it can cause an inconsistenci in the layout system and throw an exception. (Don't panic, look here: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutolayoutPG.pdf - "Auto Layout Degrades Gracefully with Unsatisfiable Constraints" 52 | */ 53 | @property (nonatomic, strong) NSPredicate *visibilityPredicate; 54 | 55 | /** 56 | @abstract Construct a field with the given parameters. 57 | @discussion Designated contrstructor for all the TLFormFields. 58 | @param fieldName The name of the fields 59 | @param displayName The title of the field 60 | @param defaultValue The value to use as default if there is no value returned form the data source 61 | @return A new instance of TLFormField 62 | */ 63 | + (instancetype)formFieldWithName:(NSString *)fieldName title:(NSString *)displayName andDefaultValue:(id)defaultValue; 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormField.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormField.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | #import "TLFormField+Protected.h" 11 | #import "TLFormAllFields.h" 12 | 13 | 14 | @implementation UIPopoverController (iPhoneSupport) 15 | 16 | + (BOOL)_popoversDisabled { 17 | return NO; 18 | } 19 | 20 | @end 21 | 22 | 23 | 24 | #pragma mark - HelpTooltipPopoverControler 25 | /************************************************** HelpTooltipPopoverControler ***************************************************************/ 26 | //This class is the content of the help tooltip. It's only a UITextView that display the value of 'helpText' property 27 | 28 | @interface HelpTooltipPopoverControler : UIViewController 29 | + (id)helpTooltipControllerWithText:(NSString *)helpText; 30 | @end 31 | 32 | @implementation HelpTooltipPopoverControler { 33 | NSString *text; 34 | UITextView *textView; 35 | } 36 | 37 | + (id)helpTooltipControllerWithText:(NSString *)helpText { 38 | HelpTooltipPopoverControler *controller = [[HelpTooltipPopoverControler alloc] init]; 39 | controller->text = helpText; 40 | controller.modalPresentationStyle = UIModalPresentationPopover; 41 | controller.popoverPresentationController.delegate = controller; 42 | return controller; 43 | } 44 | 45 | - (void)viewDidLoad { 46 | [super viewDidLoad]; 47 | 48 | textView = [[UITextView alloc] init]; 49 | textView.translatesAutoresizingMaskIntoConstraints = NO; 50 | textView.font = [UIFont systemFontOfSize:13]; 51 | textView.scrollEnabled = NO; 52 | textView.editable = NO; 53 | textView.selectable = NO; 54 | textView.text = text; 55 | [self.view addSubview:textView]; 56 | 57 | [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[textView]|" 58 | options:NSLayoutFormatAlignAllCenterY 59 | metrics:0 60 | views:NSDictionaryOfVariableBindings(textView)]]; 61 | [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textView]|" 62 | options:NSLayoutFormatAlignAllCenterX 63 | metrics:0 64 | views:NSDictionaryOfVariableBindings(textView)]]; 65 | } 66 | 67 | - (CGSize)preferredContentSize { 68 | BOOL is_iPhone = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone; 69 | return [textView sizeThatFits:CGSizeMake(is_iPhone ? 200 : 250, INFINITY)]; 70 | } 71 | 72 | - (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller { 73 | return UIModalPresentationNone; 74 | } 75 | 76 | @end 77 | 78 | 79 | #pragma mark - TLFormField 80 | //The base class for the form fields 81 | 82 | @implementation TLFormField { 83 | UIView *_titleView; 84 | NSLayoutConstraint *hiddenConstraint; 85 | UIPopoverController *popover; 86 | } 87 | 88 | #pragma mark - Field Setup 89 | 90 | + (instancetype)formFieldWithName:(NSString *)fieldName title:(NSString *)title andDefaultValue:(id)defaultValue { 91 | return [[self alloc] initWithName:fieldName title:title andDefaultValue:defaultValue]; 92 | } 93 | 94 | - (instancetype)initWithName:(NSString *)fieldName title:(NSString *)title andDefaultValue:(id)defaultValue { 95 | self = [super init]; 96 | 97 | self.borderStyle = TLFormFieldBorderNone; 98 | 99 | if (self) { 100 | self.fieldName = fieldName; 101 | self.defautValue = defaultValue; 102 | self.title = title; 103 | self.translatesAutoresizingMaskIntoConstraints = NO; 104 | self.backgroundColor = [UIColor whiteColor]; 105 | 106 | #ifdef TLFormViewLayoutDebug 107 | self.backgroundColor = [UIColor colorWithRed:(rand() % 255)/255.0 green:(rand() % 255)/255.0 blue:(rand() % 255)/255.0 alpha:1.0]; 108 | #endif 109 | } 110 | 111 | return self; 112 | } 113 | 114 | - (void)setupField:(BOOL)editing { 115 | 116 | } 117 | 118 | - (void)layoutSubviews { 119 | [super layoutSubviews]; 120 | 121 | if (self.borderStyle != TLFormFieldBorderNone) { 122 | 123 | UIBezierPath *path = [UIBezierPath bezierPath]; 124 | CGSize size = CGRectIntegral(self.bounds).size; 125 | 126 | if (self.borderStyle & TLFormFieldBorderTop) { 127 | [path moveToPoint:CGPointMake(0, 0.5)]; 128 | [path addLineToPoint:CGPointMake(size.width, 0.5)]; 129 | } 130 | 131 | if (self.borderStyle & TLFormFieldBorderRight) { 132 | [path moveToPoint:CGPointMake(size.width + 0.5, 0.5)]; 133 | [path addLineToPoint:CGPointMake(size.width + 0.5, size.height + 0.5)]; 134 | } 135 | 136 | if (self.borderStyle & TLFormFieldBorderBottom) { 137 | [path moveToPoint:CGPointMake(size.width + 0.5, size.height + 0.5)]; 138 | [path addLineToPoint:CGPointMake(0.5, size.height + 0.5)]; 139 | } 140 | 141 | if (self.borderStyle & TLFormFieldBorderLeft) { 142 | [path moveToPoint:CGPointMake(0.5, size.height)]; 143 | [path addLineToPoint:CGPointMake(0.5, 0.5)]; 144 | } 145 | 146 | CAShapeLayer *border = [CAShapeLayer layer]; 147 | border.name = @"TLFomFieldBorderLayer"; 148 | border.path = path.CGPath; 149 | border.strokeColor = [[UIColor colorWithRed:203/255.0 green:203/255.0 blue:207/255.0 alpha:1.0] CGColor]; 150 | 151 | for (CALayer *layer in self.layer.sublayers) { 152 | if ([layer.name isEqualToString:@"TLFomFieldBorderLayer"]) { 153 | [layer removeFromSuperlayer]; 154 | break; 155 | } 156 | } 157 | 158 | [self.layer addSublayer:border]; 159 | } 160 | } 161 | 162 | - (NSDictionary *)defaultMetrics { 163 | return @{@"sp": @2.0, //small padding 164 | @"np": @8.0, //normal padding 165 | @"bp": @12.0}; //big padding 166 | } 167 | 168 | #pragma mark - Hidden 169 | 170 | - (void)setHidden:(BOOL)hidden { 171 | [super setHidden:hidden]; 172 | 173 | if (hidden) { 174 | hiddenConstraint = [NSLayoutConstraint constraintWithItem:self 175 | attribute:NSLayoutAttributeHeight 176 | relatedBy:0 177 | toItem:nil 178 | attribute:NSLayoutAttributeNotAnAttribute 179 | multiplier:1.0 180 | constant:0.0]; 181 | [self.superview addConstraint:hiddenConstraint]; 182 | } else { 183 | [self.superview removeConstraint:hiddenConstraint]; 184 | hiddenConstraint = nil; 185 | } 186 | } 187 | 188 | #pragma mark - Value 189 | 190 | - (void)setValue:(id)fieldValue { 191 | 192 | } 193 | 194 | - (id)getValue { 195 | return self.defautValue; 196 | } 197 | 198 | #pragma mark - Title View 199 | 200 | - (UIView *)titleView { 201 | 202 | if (_titleView) 203 | return _titleView; 204 | 205 | UILabel *title = [[UILabel alloc] init]; 206 | title.numberOfLines = 2; 207 | title.lineBreakMode = NSLineBreakByWordWrapping; 208 | title.translatesAutoresizingMaskIntoConstraints = NO; 209 | title.text = self.title; 210 | title.tag = TLFormFieldTitleLabelTag; 211 | //Set the huggin priprity to make room for the field. 212 | [title setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; 213 | 214 | if (self.helpText) { 215 | UIButton *showHelpButton = [[UIButton alloc] init]; 216 | showHelpButton.translatesAutoresizingMaskIntoConstraints = NO; 217 | showHelpButton.titleLabel.font = [UIFont systemFontOfSize:25]; 218 | [showHelpButton setTitle:@"?" forState:UIControlStateNormal]; 219 | [showHelpButton setTitleColor:[UIColor greenColor] forState:UIControlStateNormal]; 220 | [showHelpButton addTarget:self action:@selector(showHelpAction:) forControlEvents:UIControlEventTouchUpInside]; 221 | showHelpButton.contentEdgeInsets = UIEdgeInsetsMake(1, 1, 1, 1); 222 | 223 | UIView *titleContainer = [[UIView alloc] init]; 224 | titleContainer.translatesAutoresizingMaskIntoConstraints = NO; 225 | [titleContainer addSubview:title]; 226 | [titleContainer addSubview:showHelpButton]; 227 | 228 | NSDictionary *views = NSDictionaryOfVariableBindings(title, showHelpButton); 229 | [titleContainer addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[title]-[showHelpButton]" 230 | options:NSLayoutFormatAlignAllCenterY 231 | metrics:nil 232 | views:views]]; 233 | [titleContainer addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[showHelpButton]|" 234 | options:NSLayoutFormatAlignAllCenterX 235 | metrics:nil 236 | views:views]]; 237 | _titleView = titleContainer; 238 | } else 239 | _titleView = title; 240 | 241 | return _titleView; 242 | } 243 | 244 | - (void)showHelpAction:(UIButton *)sender { 245 | popover = [[UIPopoverController alloc] initWithContentViewController:[HelpTooltipPopoverControler helpTooltipControllerWithText:self.helpText]]; 246 | CGRect finalFrame = [self convertRect:sender.frame fromView:sender.superview]; 247 | [popover presentPopoverFromRect:finalFrame inView:self permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; 248 | } 249 | 250 | @end 251 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldDateTime.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldDateTime.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/12/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSingleLine.h" 10 | 11 | @interface TLFormFieldDateTime : TLFormFieldSingleLine 12 | 13 | @property (nonatomic, strong) NSString *dateFormat; 14 | @property (nonatomic, assign) UIDatePickerMode pickerMode; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldDateTime.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldDateTime.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/12/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldDateTime.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | 14 | @implementation TLFormFieldDateTime { 15 | dispatch_once_t _onceTokenFormatter; 16 | NSDateFormatter *_formatter; 17 | UIDatePicker *picker; 18 | } 19 | 20 | - (instancetype)initWithFrame:(CGRect)frame { 21 | if (self = [super initWithFrame:frame]) { 22 | self.dateFormat = @"MMM dd, yyyy HH:mm"; 23 | self.pickerMode = UIDatePickerModeDateAndTime; 24 | } 25 | return self; 26 | } 27 | 28 | - (NSDateFormatter *)formatter { 29 | dispatch_once(&_onceTokenFormatter, ^{ 30 | _formatter = [[NSDateFormatter alloc] init]; 31 | _formatter.dateFormat = self.dateFormat; 32 | }); 33 | return _formatter; 34 | } 35 | 36 | - (void)setupFieldForEditing { 37 | picker = [[UIDatePicker alloc] init]; 38 | picker.datePickerMode = self.pickerMode; 39 | picker.translatesAutoresizingMaskIntoConstraints = NO; 40 | [picker addTarget:self action:@selector(controlValueChange) forControlEvents:UIControlEventValueChanged]; 41 | [self addSubview:picker]; 42 | self.clipsToBounds = YES; 43 | 44 | UIView *titleView = [self titleView]; 45 | NSDictionary *views = NSDictionaryOfVariableBindings(picker, titleView); 46 | 47 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-np-[titleView][picker]-np-|" 48 | options:0 49 | metrics:self.defaultMetrics 50 | views:views]]; 51 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-bp-[titleView]-bp-|" 52 | options:0 53 | metrics:self.defaultMetrics 54 | views:views]]; 55 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-np-[picker]-np-|" 56 | options:0 57 | metrics:self.defaultMetrics 58 | views:views]]; 59 | } 60 | 61 | //Get and Set value 62 | 63 | - (void)setValue:(id)fieldValue { 64 | 65 | if (!fieldValue) 66 | return; 67 | 68 | if ([fieldValue isKindOfClass:[NSDate class]]) { 69 | 70 | if (picker) 71 | picker.date = fieldValue; 72 | else 73 | self.valueViewText = [[self formatter] stringFromDate:fieldValue]; 74 | 75 | } else 76 | [NSException raise:@"Invalid field value" format:@"TLFormFieldNumeric only accept fields of type NSNumber. Suplied value: %@", fieldValue]; 77 | } 78 | 79 | - (id)getValue { 80 | if (picker) 81 | return picker.date; 82 | else 83 | return [[self formatter] dateFromString:self.valueViewText]; 84 | } 85 | 86 | //UIDatePicker value change 87 | 88 | - (void)controlValueChange { 89 | [self.formDelegate didChangeValueForField:self newValue:[self getValue]]; 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldImage.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | @interface TLFormFieldImage : TLFormField 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldImage.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldImage.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldImage.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | 14 | NSString * const TLFormFieldNoImageName = @"tlformfieldnoimage.png"; 15 | 16 | 17 | 18 | @implementation TLFormFieldImage { 19 | UIImageView *imageView; 20 | NSLayoutConstraint *imageViewHeight; 21 | //The reference used to store the image (get/set)value 22 | id imageRefValue; 23 | NSURLSessionDownloadTask *imageDownloadTask; 24 | 25 | } 26 | 27 | - (void)setupField:(BOOL)editing { 28 | [super setupField:editing]; 29 | 30 | imageView = [[UIImageView alloc] init]; 31 | imageView.contentMode = UIViewContentModeScaleAspectFit; 32 | imageView.translatesAutoresizingMaskIntoConstraints = NO; 33 | [self addSubview:imageView]; 34 | 35 | imageViewHeight = [NSLayoutConstraint constraintWithItem:imageView 36 | attribute:NSLayoutAttributeHeight 37 | relatedBy:0 38 | toItem:nil 39 | attribute:NSLayoutAttributeNotAnAttribute 40 | multiplier:1.0 constant:0]; 41 | 42 | UIButton *tapRecognizer = [[UIButton alloc] init]; 43 | tapRecognizer.translatesAutoresizingMaskIntoConstraints = NO; 44 | [tapRecognizer addTarget:self action:@selector(imageSelectedAction) forControlEvents:UIControlEventTouchUpInside]; 45 | [self addSubview:tapRecognizer]; 46 | 47 | NSDictionary *views; 48 | 49 | if (editing) { 50 | UIView *title = [self titleView]; 51 | [self addSubview:title]; 52 | 53 | UILabel *titleLabel = (UILabel *) [title viewWithTag:TLFormFieldTitleLabelTag]; 54 | titleLabel.textColor = [UIColor grayColor]; 55 | 56 | //Set the vertical hugging priority to "requiered" so the title label allways take the minimum space requiered 57 | [title setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; 58 | 59 | views = NSDictionaryOfVariableBindings(imageView, tapRecognizer, title); 60 | 61 | //Size the title to the top of the field taking all the widht 62 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-np-[title]-np-|" 63 | options:0 64 | metrics:self.defaultMetrics 65 | views:views]]; 66 | //Constraint the imageView to the size of the super view 67 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[title][imageView]|" 68 | options:0 69 | metrics:self.defaultMetrics 70 | views:views]]; 71 | 72 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-np-[imageView]-np-|" 73 | options:0 74 | metrics:self.defaultMetrics 75 | views:views]]; 76 | //Constraint the tapRecognizer to the size of the super view 77 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[title][tapRecognizer]|" 78 | options:0 79 | metrics:self.defaultMetrics 80 | views:views]]; 81 | 82 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[tapRecognizer]|" 83 | options:0 84 | metrics:self.defaultMetrics 85 | views:views]]; 86 | } else { 87 | views = NSDictionaryOfVariableBindings(imageView, tapRecognizer); 88 | 89 | //Constraint the imageView to the size of the super view 90 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]|" 91 | options:0 92 | metrics:self.defaultMetrics 93 | views:views]]; 94 | 95 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView]|" 96 | options:0 97 | metrics:self.defaultMetrics 98 | views:views]]; 99 | 100 | //Constraint the tapRecognizer to the size of the super view 101 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[tapRecognizer]|" 102 | options:0 103 | metrics:self.defaultMetrics 104 | views:views]]; 105 | 106 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[tapRecognizer]|" 107 | options:0 108 | metrics:self.defaultMetrics 109 | views:views]]; 110 | } 111 | 112 | [self setValue:self.defautValue]; 113 | } 114 | 115 | - (void)layoutSubviews { 116 | [super layoutSubviews]; 117 | 118 | //If the image is too big the "scale to aspect fit" scale the image in one dimention but not the other. This fix the case where the image fit the width but not the height. 119 | //TODO: Add the same logic for the width is trivial, the thing is how this affect all the possible layouts. This may need other approach 120 | if (imageView.image) { 121 | UIImage *image = imageView.image; 122 | CGFloat multiplier = (CGRectGetWidth(imageView.bounds) / image.size.width); 123 | CGFloat scaledHeight = multiplier * image.size.height; 124 | 125 | [self removeConstraint:imageViewHeight]; 126 | 127 | if (scaledHeight < imageView.bounds.size.height) { 128 | [imageViewHeight setConstant:scaledHeight]; 129 | [self addConstraint:imageViewHeight]; 130 | } 131 | } 132 | } 133 | 134 | - (void)setValue:(id)fieldValue { 135 | 136 | imageRefValue = fieldValue; 137 | 138 | //Update the image and invalidate the layout 139 | void (^updateImage)(UIImage *) = ^(UIImage *newImage) { 140 | imageView.image = newImage; 141 | [imageView setNeedsLayout]; 142 | }; 143 | 144 | if (!fieldValue) 145 | updateImage([UIImage imageNamed:TLFormFieldNoImageName]); 146 | 147 | else if ([fieldValue isKindOfClass:[UIImage class]]) 148 | updateImage(fieldValue); 149 | 150 | //If the value is an URL 151 | else if ([fieldValue isKindOfClass:[NSURL class]]) { 152 | 153 | //Show a spinner on the image view while download the image 154 | UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 155 | [spinner startAnimating]; 156 | spinner.translatesAutoresizingMaskIntoConstraints = NO; 157 | [imageView addSubview:spinner]; 158 | [imageView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[spinner]-|" 159 | options:0 160 | metrics:nil 161 | views:NSDictionaryOfVariableBindings(spinner)]]; 162 | [imageView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[spinner]-|" 163 | options:0 164 | metrics:nil 165 | views:NSDictionaryOfVariableBindings(spinner)]]; 166 | 167 | void (^downloadCompleteHandler)(NSURL *, NSURLResponse *, NSError *) = ^(NSURL *location, NSURLResponse *response, NSError *error){ 168 | 169 | UIImage *image; 170 | 171 | if (!error) { 172 | //We need to add the extension to the file name so get it from the 'fieldValue' and append it to the tmp file 173 | NSString *finalPath = [[location path] stringByAppendingPathExtension:[fieldValue pathExtension]]; 174 | 175 | //Try to rename the file 176 | NSError *error; 177 | [[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:finalPath error:&error]; 178 | 179 | if (!error) 180 | image = [UIImage imageWithContentsOfFile:finalPath]; 181 | else { 182 | NSLog(@"TLFormView: Error moving tmp file at url: %@ - Error: %@", location, error); 183 | image = [UIImage imageNamed:TLFormFieldNoImageName]; 184 | } 185 | 186 | } else { 187 | NSLog(@"TLFormView: Error getting image with url: %@ - Error: %@", fieldValue, error); 188 | image = [UIImage imageNamed:TLFormFieldNoImageName]; 189 | } 190 | 191 | dispatch_async(dispatch_get_main_queue(), ^{ 192 | [spinner removeFromSuperview]; 193 | updateImage(image); 194 | }); 195 | 196 | }; 197 | 198 | //Cancel any previous request before start a new one 199 | [imageDownloadTask cancel]; 200 | imageDownloadTask = [[NSURLSession sharedSession] downloadTaskWithRequest:[NSURLRequest requestWithURL:fieldValue] completionHandler:downloadCompleteHandler]; 201 | [imageDownloadTask resume]; 202 | } 203 | } 204 | 205 | - (id)getValue { 206 | return imageRefValue; 207 | } 208 | 209 | - (void)imageSelectedAction { 210 | [self.formDelegate didSelectField:self]; 211 | } 212 | 213 | @end 214 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldList.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldList.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | 12 | @protocol TLFormFieldListDelegate 13 | 14 | //In edit mode, called when a row is removed from the list. The controler should update the model. 15 | - (void)listFormField:(TLFormField *)field didDeleteRowAtIndexPath:(NSIndexPath *)indexPath; 16 | 17 | @optional 18 | 19 | //In edit mode, called to check if a row could be moved to a different possition in the list. 20 | - (BOOL)listFormField:(TLFormField *)field canMoveRowAtIndexPath:(NSIndexPath *)indexPath; 21 | 22 | //In edit mode, called when a row is moved from when position to another 23 | - (void)listFormField:(TLFormField *)field moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; 24 | 25 | //In edit mode, called when the user tap the '+' icon 26 | - (void)listFormFieldAddAction:(TLFormField *)field; 27 | 28 | @end 29 | 30 | 31 | @interface TLFormFieldList : TLFormField 32 | 33 | @property (nonatomic, weak) id delegate; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldList.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldList.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldList.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | #define kTLFormFieldListRowHeight 44.0 14 | 15 | 16 | @interface TLFormFieldList () 17 | 18 | @end 19 | 20 | 21 | @implementation TLFormFieldList { 22 | UILabel *titleLabel; 23 | UITableView *tableView; 24 | UIButton *plusButton; 25 | NSMutableArray *items; 26 | } 27 | 28 | - (BOOL)delegateCanPerformSelector:(SEL)selector { 29 | return self.delegate && [self.delegate respondsToSelector:selector]; 30 | } 31 | 32 | - (void)setupField:(BOOL)editing { 33 | [super setupField:editing]; 34 | 35 | UIView *titleView = [self titleView]; 36 | [self addSubview:titleView]; 37 | 38 | titleLabel = (UILabel *) [self viewWithTag:TLFormFieldTitleLabelTag]; 39 | if (editing) 40 | titleLabel.textColor = [UIColor grayColor]; 41 | 42 | tableView = [[UITableView alloc] init]; 43 | tableView.translatesAutoresizingMaskIntoConstraints = NO; 44 | tableView.scrollEnabled = NO; 45 | tableView.dataSource = self; 46 | tableView.editing = editing; 47 | tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 48 | [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"ItemCell"]; 49 | [self addSubview:tableView]; 50 | 51 | items = [self.defautValue mutableCopy]; 52 | 53 | NSDictionary *views; 54 | 55 | if (editing) { 56 | plusButton = [[UIButton alloc] init]; 57 | plusButton.translatesAutoresizingMaskIntoConstraints = NO; 58 | plusButton.titleLabel.font = [UIFont systemFontOfSize:35]; 59 | [plusButton setTitleColor:[UIColor greenColor] forState:UIControlStateNormal]; 60 | [plusButton setTitle:@"+" forState:UIControlStateNormal]; 61 | [plusButton addTarget:self action:@selector(plusAction:) forControlEvents:UIControlEventTouchUpInside]; 62 | 63 | [self addSubview:plusButton]; 64 | 65 | views = NSDictionaryOfVariableBindings(titleView, tableView, plusButton); 66 | 67 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-[plusButton]-bp-|" 68 | options:0 69 | metrics:self.defaultMetrics 70 | views:views]]; 71 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-np-[tableView]-np-|" 72 | options:0 73 | metrics:self.defaultMetrics 74 | views:views]]; 75 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[plusButton][tableView]-sp-|" 76 | options:0 77 | metrics:self.defaultMetrics 78 | views:views]]; 79 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleView][tableView]-sp-|" 80 | options:0 81 | metrics:self.defaultMetrics 82 | views:views]]; 83 | 84 | //Set the compresson and hugging priorities for the title and the button so it behave correctly with long text 85 | [titleView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; 86 | [plusButton setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; 87 | 88 | } else { 89 | views = NSDictionaryOfVariableBindings(titleView, tableView); 90 | 91 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-bp-|" 92 | options:0 93 | metrics:self.defaultMetrics 94 | views:views]]; 95 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-np-[tableView]-np-|" 96 | options:0 97 | metrics:self.defaultMetrics 98 | views:views]]; 99 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-np-[titleView][tableView]-np-|" 100 | options:0 101 | metrics:self.defaultMetrics 102 | views:views]]; 103 | } 104 | } 105 | 106 | - (void)setValue:(id)fieldValue { 107 | if (items.count != [fieldValue count]) 108 | [self invalidateIntrinsicContentSize]; 109 | 110 | items = [fieldValue mutableCopy]; 111 | [tableView reloadData]; 112 | } 113 | 114 | - (id)getValue { 115 | return items; 116 | } 117 | 118 | - (CGSize)intrinsicContentSize { 119 | CGFloat width = 0.0, height = 0.0; 120 | 121 | width = titleLabel.intrinsicContentSize.width + plusButton.intrinsicContentSize.width; 122 | height = MAX(titleLabel.intrinsicContentSize.height, plusButton.intrinsicContentSize.height) + (items.count * kTLFormFieldListRowHeight); 123 | 124 | return CGSizeMake(width, height); 125 | } 126 | 127 | - (void)plusAction:(id)sender { 128 | if ([self delegateCanPerformSelector:@selector(listFormFieldAddAction:)]) 129 | [self.delegate listFormFieldAddAction:self]; 130 | } 131 | 132 | #pragma mark - UITableViewDelegate 133 | 134 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 135 | return kTLFormFieldListRowHeight; 136 | } 137 | 138 | #pragma mark - UITableViewDataSource 139 | 140 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 141 | return items.count; 142 | } 143 | 144 | - (UITableViewCell *)tableView:(UITableView *)_tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 145 | UITableViewCell *cell = [_tableView dequeueReusableCellWithIdentifier:@"ItemCell"]; 146 | cell.textLabel.font = [UIFont systemFontOfSize:12]; 147 | cell.selectionStyle = UITableViewCellSelectionStyleNone; 148 | cell.textLabel.text = items[indexPath.row]; 149 | return cell; 150 | } 151 | 152 | - (void)tableView:(UITableView *)_tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 153 | [items removeObjectAtIndex:indexPath.row]; 154 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 155 | 156 | //Invalidate the intrinsic content size so the layout is recalculated. The delay is used to let the animation end before update the layout 157 | [self performSelector:@selector(invalidateIntrinsicContentSize) withObject:nil afterDelay:0.3]; 158 | 159 | [self.delegate listFormField:self didDeleteRowAtIndexPath:indexPath]; 160 | } 161 | 162 | - (BOOL)tableView:(UITableView *)_tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 163 | return tableView.editing; 164 | } 165 | 166 | - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 167 | //If this method is not implemented on the delegate return YES 168 | return ![self delegateCanPerformSelector:@selector(listFormField:canMoveRowAtIndexPath:)] || [self.delegate listFormField:self canMoveRowAtIndexPath:indexPath]; 169 | } 170 | 171 | - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { 172 | [self.delegate listFormField:self moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; 173 | } 174 | 175 | @end 176 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldMultiLine.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldMultiLine.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | @interface TLFormFieldMultiLine : TLFormField 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldMultiLine.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldMultiLine.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldMultiLine.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | 14 | @interface TLFormFieldMultiLine () 15 | 16 | @end 17 | 18 | 19 | @implementation TLFormFieldMultiLine { 20 | UITextView *textView; 21 | } 22 | 23 | - (void)setupField:(BOOL)editing { 24 | [super setupField:editing]; 25 | 26 | UIView *titleView = [self titleView]; 27 | [self addSubview:titleView]; 28 | 29 | if (editing) { 30 | UILabel *titleLabel = (UILabel *) [titleView viewWithTag:TLFormFieldTitleLabelTag]; 31 | titleLabel.textColor = [UIColor grayColor]; 32 | } 33 | 34 | textView = [[UITextView alloc] init]; 35 | textView.font = [UIFont systemFontOfSize:12]; 36 | textView.scrollEnabled = NO; 37 | textView.editable = editing; 38 | textView.translatesAutoresizingMaskIntoConstraints = NO; 39 | textView.delegate = self; 40 | [self addSubview:textView]; 41 | 42 | NSDictionary *views = NSDictionaryOfVariableBindings(titleView, textView); 43 | 44 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-bp-|" 45 | options:0 46 | metrics:self.defaultMetrics 47 | views:views]]; 48 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-np-[textView]-np-|" 49 | options:0 50 | metrics:self.defaultMetrics 51 | views:views]]; 52 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-np-[titleView][textView]-sp-|" 53 | options:0 54 | metrics:self.defaultMetrics 55 | views:views]]; 56 | 57 | [self setValue:self.defautValue]; 58 | } 59 | 60 | - (CGSize)intrinsicContentSize { 61 | CGFloat width = 0.0, height = 0.0; 62 | 63 | for (UIView *subview in self.subviews) { 64 | width += subview.intrinsicContentSize.width; 65 | height += subview.intrinsicContentSize.height; 66 | } 67 | 68 | return CGSizeMake(width, height); 69 | } 70 | 71 | //Get and Set value 72 | 73 | - (void)setValue:(id)fieldValue { 74 | textView.text = fieldValue; 75 | } 76 | 77 | - (id)getValue { 78 | return textView.text; 79 | } 80 | 81 | //UITextFieldDelegate 82 | 83 | - (void)textViewDidBeginEditing:(UITextView *)_textView { 84 | [self.formDelegate didSelectField:self]; 85 | } 86 | 87 | - (BOOL)textView:(UITextView *)_textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { 88 | 89 | NSString *newValue = nil; 90 | if (text.length > 0) 91 | newValue = [_textView.text stringByAppendingString:text]; 92 | else 93 | newValue = [_textView.text substringToIndex:_textView.text.length - 1]; 94 | 95 | [self.formDelegate didChangeValueForField:self newValue:newValue]; 96 | return YES; 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldNumeric.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldNumeric.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSingleLine.h" 10 | 11 | @interface TLFormFieldNumeric : TLFormFieldSingleLine 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldNumeric.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldNumeric.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldNumeric.h" 10 | #import "TLFormField+Protected.h" 11 | #import "NSNumber+NumberType.h" 12 | 13 | 14 | @implementation TLFormFieldNumeric{ 15 | CFNumberType numberType; 16 | } 17 | 18 | - (void)setupFieldForEditing { 19 | [super setupFieldForEditing]; 20 | self.textField.keyboardType = UIKeyboardTypeNumberPad; 21 | } 22 | 23 | //Get and Set value 24 | 25 | - (void)setValue:(id)fieldValue { 26 | 27 | if (!fieldValue) 28 | return; 29 | 30 | if ([fieldValue isKindOfClass:[NSNumber class]]) { 31 | NSNumber *value = (NSNumber *) fieldValue; 32 | numberType = [value numberType]; 33 | NSString *stringValue = [value stringValue]; 34 | 35 | if (self.textField) 36 | self.textField.text = stringValue; 37 | else 38 | self.valueViewText = stringValue; 39 | } else 40 | [NSException raise:@"Invalid field value" format:@"TLFormFieldNumeric only accept fields of type NSNumber. Suplied value: %@", fieldValue]; 41 | } 42 | 43 | - (id)getValue { 44 | NSString *stringValue; 45 | 46 | if (self.textField) 47 | stringValue = self.textField.text; 48 | else 49 | stringValue = self.valueViewText; 50 | 51 | return [NSNumber numberOfType:numberType withValue:stringValue]; 52 | } 53 | 54 | //UITextFieldDelegate 55 | 56 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { 57 | [self.formDelegate didSelectField:self]; 58 | return YES; 59 | } 60 | 61 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { 62 | 63 | id newValue = nil; 64 | 65 | if (string.length > 0) 66 | newValue = [textField.text stringByAppendingString:string]; 67 | else 68 | newValue = [textField.text substringToIndex:textField.text.length - 1]; 69 | 70 | //Translate the value to an NSNumber in the same domain as the one given to the fild as initial value 71 | newValue = [NSNumber numberOfType:numberType withValue:newValue]; 72 | [self.formDelegate didChangeValueForField:self newValue:newValue]; 73 | 74 | return YES; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldSelect.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldSelect.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSingleLine.h" 10 | 11 | @interface TLFormFieldSelect : TLFormFieldSingleLine 12 | 13 | //The list of values to show in the segmented control 14 | @property (nonatomic, strong) NSArray *choicesValues; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldSelect.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldSelect.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSelect.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | 14 | @implementation TLFormFieldSelect { 15 | UISegmentedControl *segmented; 16 | } 17 | 18 | - (void)setupFieldForEditing { 19 | 20 | segmented = [[UISegmentedControl alloc] init]; 21 | segmented.tag = TLFormFieldValueLabelTag; 22 | segmented.translatesAutoresizingMaskIntoConstraints = NO; 23 | [segmented addTarget:self action:@selector(controlValueChange) forControlEvents:UIControlEventValueChanged]; 24 | 25 | for (NSString *choice in self.choicesValues) 26 | [segmented insertSegmentWithTitle:choice atIndex:[self.choicesValues indexOfObject:choice] animated:NO]; 27 | 28 | [self addSubview:segmented]; 29 | 30 | UIView *titleView = [self titleView]; 31 | NSDictionary *views = NSDictionaryOfVariableBindings(titleView, segmented); 32 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-sp-[titleView]-sp-[segmented]-np-|" 33 | options:NSLayoutFormatAlignAllCenterX 34 | metrics:self.defaultMetrics 35 | views:views]]; 36 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-bp-|" 37 | options:NSLayoutFormatAlignAllCenterY 38 | metrics:self.defaultMetrics 39 | views:views]]; 40 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[segmented]-bp-|" 41 | options:NSLayoutFormatAlignAllCenterY 42 | metrics:self.defaultMetrics 43 | views:views]]; 44 | } 45 | 46 | //Get and Set value 47 | 48 | - (void)setValue:(id)fieldValue { 49 | 50 | if (!fieldValue) 51 | return; 52 | 53 | if ([fieldValue isKindOfClass:[NSString class]]) { 54 | NSString *stringValue = (NSString *) fieldValue; 55 | 56 | if (segmented) { 57 | for (int i = 0; i < [segmented numberOfSegments]; i++) { 58 | if ([stringValue isEqualToString:[segmented titleForSegmentAtIndex:i]]) { 59 | segmented.selectedSegmentIndex = i; 60 | break; 61 | } 62 | } 63 | } else 64 | self.valueViewText = stringValue; 65 | 66 | } else 67 | [NSException raise:@"Invalid field value" format:@"TLFormFieldSelect only accept fields of type NSString. Suplied value: %@", fieldValue]; 68 | } 69 | 70 | - (id)getValue { 71 | if (segmented) 72 | return [segmented titleForSegmentAtIndex:segmented.selectedSegmentIndex]; 73 | else 74 | return self.valueViewText; 75 | } 76 | 77 | //UISegmented value change 78 | 79 | - (void)controlValueChange { 80 | [self.formDelegate didChangeValueForField:self newValue:[self getValue]]; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldSingleLine.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldSingleLine.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | 12 | @interface TLFormFieldSingleLine : TLFormField 13 | 14 | @end 15 | 16 | 17 | @interface TLFormFieldSingleLine (Protected) 18 | 19 | @property (nonatomic, assign) NSString *valueViewText; 20 | @property (nonatomic, readonly) UITextField *textField; 21 | 22 | - (void)setupFieldForEditing; 23 | 24 | @end -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldSingleLine.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldSingleLine.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSingleLine.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | @interface TLFormFieldSingleLine () 14 | @end 15 | 16 | 17 | @implementation TLFormFieldSingleLine { 18 | UITextField *textField; 19 | } 20 | 21 | - (void)setupField:(BOOL)editing { 22 | [super setupField:editing]; 23 | 24 | UIView *titleView = [self titleView]; 25 | [self addSubview:titleView]; 26 | 27 | if (editing) { 28 | 29 | UILabel *titleLabel = (UILabel *) [titleView viewWithTag:TLFormFieldTitleLabelTag]; 30 | titleLabel.textColor = [UIColor grayColor]; 31 | [self setupFieldForEditing]; 32 | 33 | } else { 34 | 35 | UILabel *valueLabel = [[UILabel alloc] init]; 36 | valueLabel.tag = TLFormFieldValueLabelTag; 37 | valueLabel.numberOfLines = 1; 38 | valueLabel.textAlignment = NSTextAlignmentRight; 39 | valueLabel.translatesAutoresizingMaskIntoConstraints = NO; 40 | 41 | [self addSubview:valueLabel]; 42 | 43 | //Adjust the compression resistance for each view so the labels resize always in the same way when the size of the container change. Without this set explicitly 44 | //the behavior is inconsistent 45 | [titleView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; 46 | [valueLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; 47 | 48 | NSDictionary *views = NSDictionaryOfVariableBindings(titleView, valueLabel); 49 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-bp-[valueLabel]-bp-|" 50 | options:NSLayoutFormatAlignAllCenterY 51 | metrics:self.defaultMetrics 52 | views:views]]; 53 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-sp-[titleView]-sp-|" 54 | options:NSLayoutFormatAlignAllCenterY 55 | metrics:self.defaultMetrics 56 | views:views]]; 57 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-sp-[valueLabel]-sp-|" 58 | options:NSLayoutFormatAlignAllCenterY 59 | metrics:self.defaultMetrics 60 | views:views]]; 61 | } 62 | 63 | [self setValue:self.defautValue]; 64 | } 65 | 66 | - (CGSize)intrinsicContentSize { 67 | CGFloat width = 0.0, height = 0.0; 68 | 69 | for (UIView *subview in self.subviews) { 70 | width += subview.intrinsicContentSize.width; 71 | //Don't acumulte the height, use the maximum 72 | height = MAX(height, subview.intrinsicContentSize.height); 73 | } 74 | 75 | return CGSizeMake(width, height); 76 | } 77 | 78 | //Get and Set value 79 | 80 | - (void)setValue:(id)fieldValue { 81 | 82 | if (!fieldValue) 83 | return; 84 | 85 | NSString *stringValue; 86 | if ([fieldValue isKindOfClass:[NSString class]] == NO) 87 | stringValue = [fieldValue stringValue]; 88 | else 89 | stringValue = fieldValue; 90 | 91 | if (textField) 92 | textField.text = stringValue; 93 | else 94 | self.valueViewText = stringValue; 95 | } 96 | 97 | - (id)getValue { 98 | if (textField) 99 | return textField.text; 100 | else 101 | return self.valueViewText; 102 | } 103 | 104 | //UITextFieldDelegate 105 | 106 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { 107 | [self.formDelegate didSelectField:self]; 108 | return YES; 109 | } 110 | 111 | - (BOOL)textField:(UITextField *)_textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { 112 | 113 | id newValue = nil; 114 | 115 | if (string.length > 0) 116 | newValue = [_textField.text stringByAppendingString:string]; 117 | else 118 | newValue = [_textField.text substringToIndex:_textField.text.length - 1]; 119 | 120 | [self.formDelegate didChangeValueForField:self newValue:newValue]; 121 | 122 | return YES; 123 | } 124 | 125 | @end 126 | 127 | 128 | @implementation TLFormFieldSingleLine (Protected) 129 | 130 | - (NSString *)valueViewText { 131 | return [[self viewWithTag:TLFormFieldValueLabelTag] performSelector:@selector(text)]; 132 | } 133 | 134 | - (void)setValueViewText:(NSString *)valueViewText { 135 | [[self viewWithTag:TLFormFieldValueLabelTag] performSelector:@selector(setText:) withObject:valueViewText]; 136 | } 137 | 138 | - (UITextField *)textField { 139 | return textField; 140 | } 141 | 142 | - (void)setupFieldForEditing { 143 | 144 | textField = [[UITextField alloc] init]; 145 | textField.textAlignment = NSTextAlignmentRight; 146 | textField.translatesAutoresizingMaskIntoConstraints = NO; 147 | textField.borderStyle = UITextBorderStyleNone; 148 | textField.delegate = self; 149 | [self addSubview:textField]; 150 | 151 | UIView *titleView = [self titleView]; 152 | NSDictionary *views = NSDictionaryOfVariableBindings(titleView, textField); 153 | 154 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-sp-[titleView]-sp-|" 155 | options:NSLayoutFormatAlignAllCenterX 156 | metrics:self.defaultMetrics 157 | views:views]]; 158 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-np-[textField]-bp-|" 159 | options:NSLayoutFormatAlignAllCenterY 160 | metrics:self.defaultMetrics 161 | views:views]]; 162 | } 163 | 164 | @end -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldTitle.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldTitle.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | @interface TLFormFieldTitle : TLFormField 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldTitle.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldTitle.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldTitle.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | 14 | @implementation TLFormFieldTitle { 15 | UILabel *titleLabel; 16 | } 17 | 18 | - (void)setupField:(BOOL)editing { 19 | [super setupField:editing]; 20 | 21 | titleLabel = [[UILabel alloc] init]; 22 | titleLabel.translatesAutoresizingMaskIntoConstraints = NO; 23 | titleLabel.textAlignment = NSTextAlignmentCenter; 24 | titleLabel.font = [UIFont boldSystemFontOfSize:18.0]; 25 | titleLabel.numberOfLines = 0; 26 | 27 | self.backgroundColor = [UIColor clearColor]; 28 | [self addSubview:titleLabel]; 29 | 30 | NSDictionary *views = NSDictionaryOfVariableBindings(titleLabel); 31 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleLabel]|" 32 | options:0 33 | metrics:self.defaultMetrics 34 | views:views]]; 35 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-np-[titleLabel]-np-|" 36 | options:0 37 | metrics:self.defaultMetrics 38 | views:views]]; 39 | 40 | [self setValue:self.defautValue]; 41 | } 42 | 43 | - (TLFormBorderStyleMask)borderStyle { 44 | return TLFormFieldBorderNone; 45 | } 46 | 47 | - (CGSize)intrinsicContentSize { 48 | return titleLabel.intrinsicContentSize; 49 | } 50 | 51 | - (id)getValue { 52 | return titleLabel.text; 53 | } 54 | 55 | - (void)setValue:(id)fieldValue { 56 | titleLabel.text = fieldValue; 57 | } 58 | 59 | @end 60 | 61 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldYesNo.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldYesNo.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldSingleLine.h" 10 | 11 | @interface TLFormFieldYesNo : TLFormFieldSingleLine 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Pod/Classes/Fields/TLFormFieldYesNo.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormFieldYesNo.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 5/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormFieldYesNo.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | @implementation TLFormFieldYesNo { 14 | UISwitch *yesNoSelect; 15 | } 16 | 17 | - (void)setupFieldForEditing { 18 | 19 | yesNoSelect = [[UISwitch alloc] init]; 20 | yesNoSelect.tag = TLFormFieldValueLabelTag; 21 | yesNoSelect.translatesAutoresizingMaskIntoConstraints = NO; 22 | [yesNoSelect addTarget:self action:@selector(controlValueChange) forControlEvents:UIControlEventValueChanged]; 23 | 24 | [self addSubview:yesNoSelect]; 25 | 26 | UIView *titleView = [self titleView]; 27 | NSDictionary *views = NSDictionaryOfVariableBindings(titleView, yesNoSelect); 28 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-bp-[titleView]-bp-[yesNoSelect]-bp-|" 29 | options:NSLayoutFormatAlignAllCenterY 30 | metrics:self.defaultMetrics 31 | views:views]]; 32 | 33 | //The vertical constraints needs to be set with explicit contraints because the visual format language can't express this rules. 34 | [self addConstraint:[NSLayoutConstraint constraintWithItem:yesNoSelect 35 | attribute:NSLayoutAttributeCenterY 36 | relatedBy:NSLayoutRelationEqual 37 | toItem:self 38 | attribute:NSLayoutAttributeCenterY 39 | multiplier:1.0 constant:0.0]]; 40 | [self addConstraint:[NSLayoutConstraint constraintWithItem:titleView 41 | attribute:NSLayoutAttributeCenterY 42 | relatedBy:NSLayoutRelationEqual 43 | toItem:self 44 | attribute:NSLayoutAttributeCenterY 45 | multiplier:1.0 constant:0.0]]; 46 | } 47 | 48 | //Get and Set value 49 | 50 | - (void)setValue:(id)fieldValue { 51 | 52 | if (!fieldValue) 53 | return; 54 | 55 | if ([fieldValue isKindOfClass:[NSNumber class]]) { 56 | NSNumber *value = (NSNumber *) fieldValue; 57 | 58 | if (yesNoSelect) 59 | yesNoSelect.on = [value boolValue]; 60 | else 61 | self.valueViewText = [value boolValue] ? @"Yes" : @"No"; 62 | 63 | } else 64 | [NSException raise:@"Invalid field value" format:@"TLFormFieldYesNo only accept fields of type NSNumber (boolean). Suplied value: %@", fieldValue]; 65 | } 66 | 67 | - (id)getValue { 68 | if (yesNoSelect) 69 | return @(yesNoSelect.on); 70 | else 71 | return @([self.valueViewText boolValue]); 72 | } 73 | 74 | //UISwitch value change 75 | 76 | - (void)controlValueChange { 77 | [self.formDelegate didChangeValueForField:self newValue:[self getValue]]; 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /Pod/Classes/Style/TLFormField+UIAppearance.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormField+UIApearance.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/26/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField.h" 10 | 11 | 12 | @interface TLFormField (UIAppearance) 13 | 14 | - (void)setTitleFont:(UIFont *)font UI_APPEARANCE_SELECTOR; 15 | - (void)setTitleTextColor:(UIColor *)color UI_APPEARANCE_SELECTOR; 16 | - (void)setTitleBackgroundColor:(UIColor *)color UI_APPEARANCE_SELECTOR; 17 | 18 | - (void)setValueFont:(UIFont *)font UI_APPEARANCE_SELECTOR; 19 | - (void)setValueTextColor:(UIColor *)color UI_APPEARANCE_SELECTOR; 20 | - (void)setValueBackgroundColor:(UIColor *)color UI_APPEARANCE_SELECTOR; 21 | 22 | - (void)setBorderStyleMask:(TLFormBorderStyleMask)borderMask UI_APPEARANCE_SELECTOR; 23 | 24 | 25 | + (UIButton *) helpButtonAppearance; 26 | 27 | + (UIButton *) addButtonAppearance; 28 | 29 | + (UISegmentedControl *) segmentedAppearance; 30 | 31 | + (UISwitch *) switchAppearance; 32 | 33 | + (UITextField *) textFieldAppearance; 34 | 35 | @end 36 | 37 | -------------------------------------------------------------------------------- /Pod/Classes/Style/TLFormField+UIAppearance.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormField+UIApearance.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/26/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormField+UIAppearance.h" 10 | #import "TLFormField+Protected.h" 11 | #import "TLFormAllFields.h" 12 | 13 | 14 | int const TLFormFieldTitleLabelTag = 42001; 15 | int const TLFormFieldValueLabelTag = 42002; 16 | 17 | 18 | @implementation TLFormField (UIAppearance) 19 | 20 | #pragma mark - Title label accessors 21 | 22 | - (UILabel *)titleLabel { 23 | return (UILabel *) [self viewWithTag:TLFormFieldTitleLabelTag]; 24 | } 25 | 26 | - (void)setTitleFont:(UIFont *)titleFont { 27 | [[self titleLabel] setFont:titleFont]; 28 | } 29 | 30 | - (void)setTitleTextColor:(UIColor *)color { 31 | [[self titleLabel] setTextColor:color]; 32 | } 33 | 34 | - (void)setTitleBackgroundColor:(UIColor *)color { 35 | [[self titleLabel] setBackgroundColor:color]; 36 | } 37 | 38 | #pragma mark - Value label accessors 39 | 40 | - (UILabel *)valueLabel { 41 | return (UILabel *) [self viewWithTag:TLFormFieldValueLabelTag]; 42 | } 43 | 44 | - (void)setValueFont:(UIFont *)font { 45 | [[self valueLabel] setFont:font]; 46 | } 47 | 48 | - (void)setValueTextColor:(UIColor *)color { 49 | [[self valueLabel] setTextColor:color]; 50 | } 51 | 52 | - (void)setValueBackgroundColor:(UIColor *)color { 53 | [[self valueLabel] setBackgroundColor:color]; 54 | } 55 | 56 | #pragma mark - Border Style 57 | 58 | - (void)setBorderStyleMask:(TLFormBorderStyleMask)borderMask { 59 | self.borderStyle = borderMask; 60 | } 61 | 62 | #pragma mark - Appearance getters 63 | 64 | + (UIButton *)helpButtonAppearance { 65 | return [UIButton appearanceWhenContainedIn:[TLFormField class], nil]; 66 | } 67 | 68 | + (UIButton *)addButtonAppearance { 69 | return [UIButton appearanceWhenContainedIn:[TLFormFieldList class], nil]; 70 | } 71 | 72 | + (UISegmentedControl *)segmentedAppearance { 73 | return [UISegmentedControl appearanceWhenContainedIn:[TLFormFieldSingleLine class], nil]; 74 | } 75 | 76 | + (UISwitch *)switchAppearance { 77 | return [UISwitch appearanceWhenContainedIn:[TLFormFieldSingleLine class], nil]; 78 | } 79 | 80 | + (UITextField *)textFieldAppearance { 81 | return [UITextField appearanceWhenContainedIn:[TLFormFieldSingleLine class], nil]; 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /Pod/Classes/TLFormModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormModel.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TLFormView.h" 11 | 12 | 13 | @interface TLFormSeparator : NSObject @end 14 | TLFormSeparator * TLFormSeparatorValue (); 15 | 16 | 17 | @interface TLFormText : NSString @end 18 | TLFormText * TLFormTextValue(NSString *text); 19 | 20 | @interface TLFormLongText : NSString @end 21 | TLFormLongText * TLFormLongTextValue(NSString *longText); 22 | 23 | @interface TLFormTitle : NSString @end 24 | TLFormTitle * TLFormTitleValue (NSString *title); 25 | 26 | 27 | @interface TLFormNumber : NSNumber @end 28 | TLFormNumber * TLFormNumberValue (NSNumber *number); 29 | 30 | @interface TLFormBoolean : NSNumber @end 31 | TLFormBoolean * TLFormBooleanValue (BOOL boolean); 32 | 33 | @interface TLFormEnumerated : NSDictionary @end 34 | extern NSString * const TLFormEnumeratedSelectedValue; 35 | extern NSString * const TLFormEnumeratedAllValues; 36 | 37 | TLFormEnumerated * TLFormEnumeratedValue (id current, NSArray *all); 38 | 39 | 40 | @interface TLFormList : NSArray @end 41 | TLFormList * TLFormListValue(NSArray *array); 42 | 43 | 44 | @interface TLFormImage : NSObject @end 45 | TLFormImage * TLFormImageValue (NSObject *urlOrImage); 46 | 47 | @interface TLFormDateTime : NSDate @end 48 | TLFormDateTime * TLFormDateTimeValue (NSDate *date); 49 | 50 | /** 51 | @abstract Model a form using an object. 52 | @discussion This class is intended for subclassing. It infer the implementation of 'TLFormViewDataSource' from his own properties and the 'TLFormViewDelegate' imlementation mantains the property values in synch. 53 | */ 54 | @interface TLFormModel : NSObject 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Pod/Classes/TLFormModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormModel.m 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 2/24/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | #import "TLFormModel.h" 10 | #import 11 | #import "TLFormAllFields.h" 12 | 13 | 14 | @implementation TLFormSeparator : NSObject @end 15 | TLFormSeparator * TLFormSeparatorValue () { 16 | return [TLFormSeparator new]; 17 | } 18 | 19 | @implementation TLFormText : NSString @end 20 | TLFormText * TLFormTextValue(NSString *text) { 21 | return (TLFormText *) [text copy]; 22 | } 23 | 24 | @implementation TLFormLongText : NSString @end 25 | TLFormLongText * TLFormLongTextValue(NSString *longText) { 26 | return (TLFormLongText *) [longText copy]; 27 | } 28 | 29 | @implementation TLFormTitle : NSString @end 30 | TLFormTitle * TLFormTitleValue (NSString *title) { 31 | return (TLFormTitle *) [title copy]; 32 | } 33 | 34 | @implementation TLFormNumber : NSNumber @end 35 | TLFormNumber * TLFormNumberValue (NSNumber *number) { 36 | return (TLFormNumber *) [number copy]; 37 | } 38 | 39 | @implementation TLFormBoolean : NSNumber @end 40 | TLFormBoolean * TLFormBooleanValue (BOOL boolean) { 41 | return (TLFormBoolean *) [TLFormBoolean numberWithBool:boolean]; 42 | } 43 | 44 | @implementation TLFormEnumerated : NSDictionary @end 45 | NSString * const TLFormEnumeratedSelectedValue = @"TLFormEnumeratedSelectedValue"; 46 | NSString * const TLFormEnumeratedAllValues = @"TLFormEnumeratedAllValues"; 47 | 48 | TLFormEnumerated * TLFormEnumeratedValue (id current, NSArray *all) { 49 | return (TLFormEnumerated *) @{TLFormEnumeratedSelectedValue: current, 50 | TLFormEnumeratedAllValues: all}; 51 | } 52 | 53 | @implementation TLFormList : NSArray @end 54 | TLFormList * TLFormListValue(NSArray *array) { 55 | return (TLFormList *) [array copy]; 56 | } 57 | 58 | @implementation TLFormImage : NSObject @end 59 | TLFormImage * TLFormImageValue (NSObject *urlOrImage) { 60 | if ([urlOrImage isKindOfClass:[UIImage class]] || [urlOrImage isKindOfClass:[NSURL class]]) { 61 | return (TLFormImage *) [urlOrImage copy]; 62 | } else { 63 | [NSException raise:@"Invalid image type" format:@"Should be UIImage or NSURL but it is: %@", NSStringFromClass([urlOrImage class])]; 64 | return nil; 65 | } 66 | } 67 | 68 | @implementation TLFormDateTime : NSDate @end 69 | TLFormDateTime * TLFormDateTimeValue (NSDate *date) { 70 | return (TLFormDateTime *) [date copy]; 71 | } 72 | 73 | 74 | typedef enum { 75 | TLFormValueTypeUnknow = 0, 76 | TLFormValueTypeSeparator, 77 | TLFormValueTypeText, 78 | TLFormValueTypeLongText, 79 | TLFormValueTypeTitle, 80 | TLFormValueTypeNumber, 81 | TLFormValueTypeBoolean, 82 | TLFormValueTypeEnumerated, 83 | TLFormValueTypeList, 84 | TLFormValueTypeImage, 85 | TLFormValueTypeDateTime 86 | } TLFormValueType; 87 | 88 | 89 | 90 | @interface TLPropertyInfo : NSObject 91 | 92 | @property (nonatomic, strong) NSString *name; 93 | @property (nonatomic, readonly) Class fieldClass; 94 | @property (nonatomic, readonly) NSString *title; 95 | @property (nonatomic, readonly) TLFormValueType valueType; 96 | 97 | + (instancetype)withObjcProperty:(objc_property_t)property; 98 | 99 | @end 100 | 101 | @implementation TLPropertyInfo 102 | 103 | + (instancetype)withObjcProperty:(objc_property_t)property { 104 | 105 | TLPropertyInfo *pi = nil; 106 | //Example value: T@"PROPERTY_TYPE",&,N,V_avatar 107 | NSString *propertyType = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding]; 108 | 109 | //Parse the property type string to get the class 110 | if (propertyType.length > 4) { 111 | 112 | propertyType = [propertyType substringFromIndex:3]; 113 | NSUInteger rightQuoteIdx = [propertyType rangeOfString:@"\""].location; 114 | 115 | if (rightQuoteIdx != NSNotFound) { 116 | propertyType = [propertyType substringToIndex:rightQuoteIdx]; 117 | 118 | TLFormValueType valueType = [TLPropertyInfo valueTypeFromString:propertyType]; 119 | if (valueType != TLFormValueTypeUnknow) { 120 | 121 | pi = [[TLPropertyInfo alloc] init]; 122 | pi.name = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; 123 | pi->_valueType = valueType; 124 | [pi setupFieldInfo]; 125 | } 126 | } 127 | } 128 | 129 | return pi; 130 | } 131 | 132 | + (TLFormValueType)valueTypeFromString:(NSString *)stringType { 133 | //A quick way to turn a class name to an enumerated type. 134 | 135 | NSUInteger idx = [@[@"TLFormSeparator", 136 | @"TLFormText", 137 | @"TLFormLongText", 138 | @"TLFormTitle", 139 | @"TLFormNumber", 140 | @"TLFormBoolean", 141 | @"TLFormEnumerated", 142 | @"TLFormList", 143 | @"TLFormImage", 144 | @"TLFormDateTime"] indexOfObject:stringType]; 145 | 146 | if (idx != NSNotFound) 147 | return (TLFormValueType) idx + 1; 148 | else 149 | return TLFormValueTypeUnknow; 150 | } 151 | 152 | - (void)setupFieldInfo { 153 | 154 | //Map the value types to field and input types to define de behaviour of each type of value 155 | 156 | switch (self.valueType) { 157 | 158 | case TLFormValueTypeSeparator: 159 | _fieldClass = [TLFormFieldTitle class]; 160 | break; 161 | 162 | case TLFormValueTypeLongText: 163 | _fieldClass = [TLFormFieldMultiLine class]; 164 | break; 165 | 166 | case TLFormValueTypeTitle: 167 | _fieldClass = [TLFormFieldTitle class]; 168 | break; 169 | 170 | case TLFormValueTypeText: 171 | _fieldClass = [TLFormFieldSingleLine class]; 172 | break; 173 | 174 | case TLFormValueTypeNumber: 175 | _fieldClass = [TLFormFieldNumeric class]; 176 | break; 177 | 178 | case TLFormValueTypeBoolean: 179 | _fieldClass = [TLFormFieldYesNo class]; 180 | break; 181 | 182 | case TLFormValueTypeEnumerated: 183 | _fieldClass = [TLFormFieldSelect class]; 184 | break; 185 | 186 | case TLFormValueTypeList: 187 | _fieldClass = [TLFormFieldList class]; 188 | break; 189 | 190 | case TLFormValueTypeImage: 191 | _fieldClass = [TLFormFieldImage class]; 192 | break; 193 | 194 | case TLFormValueTypeDateTime: 195 | _fieldClass = [TLFormFieldDateTime class]; 196 | break; 197 | 198 | default: 199 | [NSException raise:@"Invalid form value type" format:@"Raw value: %d", self.valueType]; 200 | break; 201 | } 202 | 203 | 204 | //The separator are a TLFormTitle with an empty title 205 | if (self.valueType == TLFormValueTypeSeparator) 206 | _title = @" "; 207 | else { 208 | //For the rest of te fields the title is calculated from the property name converting the "_" in spaces and 209 | //capitalising all the words (ex: "some_property_name" -> "Some Property Name") 210 | for (NSString *titleComp in [self.name componentsSeparatedByString:@"_"]) { 211 | if (_title.length > 0) 212 | _title = [_title stringByAppendingFormat:@" %@", [titleComp capitalizedString]]; 213 | else 214 | _title = [titleComp capitalizedString]; 215 | } 216 | } 217 | } 218 | 219 | @end 220 | 221 | 222 | 223 | @interface TLFormModel () 224 | 225 | @end 226 | 227 | 228 | @implementation TLFormModel { 229 | NSMutableArray *propertiesInfo; 230 | NSArray *propertiesIndex; 231 | } 232 | 233 | - (TLPropertyInfo *)infoFormFieldWithName:(NSString *)fieldName { 234 | return propertiesInfo[[propertiesIndex indexOfObject:fieldName]]; 235 | } 236 | 237 | #pragma mark - TLFormViewDataSource 238 | 239 | - (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form { 240 | 241 | //This is the first method called to setup the form so we parse our properties and fill the 'propertiesInfo' list with objects PropertyInfo 242 | 243 | unsigned int count = 0; 244 | objc_property_t *properties = class_copyPropertyList([self class], &count); 245 | propertiesInfo = [NSMutableArray arrayWithCapacity:count]; 246 | 247 | for (int i = 0; i < count; i++ ) { 248 | 249 | objc_property_t property = properties[i]; 250 | TLPropertyInfo *propertyInfo = [TLPropertyInfo withObjcProperty:property]; 251 | 252 | if (propertyInfo) 253 | [propertiesInfo addObject:propertyInfo]; 254 | } 255 | 256 | free(properties); 257 | 258 | //The 'name' in PropertyInfo is the 'fieldName' in the TLFormField 259 | propertiesIndex = [propertiesInfo valueForKey:@"name"]; 260 | return propertiesIndex; 261 | } 262 | 263 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName { 264 | 265 | TLPropertyInfo *fieldInfo = [self infoFormFieldWithName:fieldName]; 266 | id value = nil; 267 | NSArray *choices; 268 | 269 | if (fieldInfo.valueType == TLFormValueTypeTitle) 270 | value = fieldInfo.title; 271 | else { 272 | value = [self valueForKey:fieldName]; 273 | 274 | //Get the choices for the enumerated type 275 | if (fieldInfo.valueType == TLFormValueTypeEnumerated) { 276 | choices = value[TLFormEnumeratedAllValues]; 277 | value = value[TLFormEnumeratedSelectedValue]; 278 | } 279 | } 280 | 281 | TLFormField *field = [fieldInfo.fieldClass formFieldWithName:fieldName title:fieldInfo.title andDefaultValue:value]; 282 | 283 | //Set the properties specific for the single line field class 284 | if ([fieldInfo.fieldClass isSubclassOfClass:[TLFormFieldSelect class]]) { 285 | 286 | TLFormFieldSelect *singleLineField = (TLFormFieldSelect *) field; 287 | singleLineField.choicesValues = choices; 288 | 289 | } else if ([fieldInfo.fieldClass isSubclassOfClass:[TLFormFieldList class]]) { 290 | 291 | TLFormFieldList *listField = (TLFormFieldList *) field; 292 | listField.delegate = self; 293 | } 294 | 295 | //Set the top and bottom borders 296 | field.borderStyle = TLFormFieldBorderTop | TLFormFieldBorderBottom; 297 | 298 | return field; 299 | } 300 | 301 | - (id)formView:(TLFormView *)form valueForFieldWithName:(NSString *)fieldName { 302 | TLPropertyInfo *fieldInfo = [self infoFormFieldWithName:fieldName]; 303 | id value = [self valueForKey:fieldName]; 304 | 305 | switch (fieldInfo.valueType) { 306 | case TLFormValueTypeEnumerated: 307 | return value[TLFormEnumeratedSelectedValue]; 308 | 309 | case TLFormValueTypeTitle: 310 | return fieldInfo.title; 311 | 312 | default: 313 | return value; 314 | } 315 | } 316 | 317 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form { 318 | 319 | //The default layout is present the fields in a column in the same order that the property declaration has. 320 | 321 | NSMutableArray *constraints = [NSMutableArray array]; 322 | 323 | for (NSUInteger i = 0; i < propertiesIndex.count; i++) { 324 | 325 | NSString *propertyName = propertiesIndex[i]; 326 | NSString *verticalConsFormat; 327 | 328 | if ([propertiesIndex firstObject] == propertyName) 329 | verticalConsFormat = @"V:|-margin-[%@(>=44)]"; 330 | 331 | else { 332 | 333 | NSString *previousProperty = propertiesIndex[i - 1]; 334 | verticalConsFormat = [NSString stringWithFormat:@"V:[%@(>=44)]-(==0)-[%%@(>=44)]", previousProperty]; 335 | 336 | if ([propertiesIndex lastObject] == propertyName) 337 | verticalConsFormat = [verticalConsFormat stringByAppendingString:@"-margin-|"]; 338 | } 339 | 340 | [constraints addObject:[NSString stringWithFormat:verticalConsFormat, propertyName]]; 341 | [constraints addObject:[NSString stringWithFormat:@"|-margin-[%@]-margin-|", propertyName]]; 342 | } 343 | 344 | return constraints; 345 | } 346 | 347 | #pragma mark - TLFormViewDelegate 348 | 349 | - (void)formView:(TLFormView *)form didChangeValueForField:(TLFormField *)field newValue:(id)value { 350 | NSString *fieldName = field.fieldName; 351 | TLPropertyInfo *fieldInfo = [self infoFormFieldWithName:fieldName]; 352 | 353 | if (fieldInfo.valueType == TLFormValueTypeEnumerated) { 354 | NSMutableDictionary *enumValue = [[self valueForKey:fieldName] mutableCopy]; 355 | [enumValue setObject:value forKey:TLFormEnumeratedSelectedValue]; 356 | [self setValue:enumValue forKey:fieldName]; 357 | } else 358 | [self setValue:value forKey:fieldName]; 359 | } 360 | 361 | #pragma mark - TLFormFieldListDelegate 362 | 363 | - (void)listFormField:(TLFormField *)field didDeleteRowAtIndexPath:(NSIndexPath *)indexPath { 364 | NSMutableArray *listValue = [[self valueForKey:field.fieldName] mutableCopy]; 365 | [listValue removeObjectAtIndex:indexPath.row]; 366 | [self setValue:listValue forKey:field.fieldName]; 367 | } 368 | 369 | - (BOOL)listFormField:(TLFormField *)field canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 370 | return YES; 371 | } 372 | 373 | - (void)listFormField:(TLFormField *)field moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { 374 | NSMutableArray *listValue = [[self valueForKey:field.fieldName] mutableCopy]; 375 | 376 | id sourceObj = listValue[sourceIndexPath.row]; 377 | id destObj = listValue[destinationIndexPath.row]; 378 | 379 | [listValue replaceObjectAtIndex:sourceIndexPath.row withObject:destObj]; 380 | [listValue replaceObjectAtIndex:destinationIndexPath.row withObject:sourceObj]; 381 | 382 | [self setValue:listValue forKey:field.fieldName]; 383 | } 384 | 385 | @end 386 | 387 | -------------------------------------------------------------------------------- /Pod/Classes/TLFormView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormView.h 3 | // TLFormView 4 | // 5 | // Created by Bruno Berisso on 3/19/14. 6 | // Copyright (c) 2014 Gathered Table LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TLFormField.h" 11 | 12 | 13 | /************************************************************************************************************************************************/ 14 | /******************************************************* TLFormView Delegate and Data Source **************************************************/ 15 | /************************************************************************************************************************************************/ 16 | 17 | /** 18 | @abstract Basic interaction with the fields 19 | @discussion This delegate is fired when the user interact with any of the fields. 20 | */ 21 | @class TLFormView; 22 | @protocol TLFormViewDelegate 23 | 24 | @optional 25 | 26 | /** 27 | @abstract Called when the field is selected. 28 | @discussion When this method is fired depends of the TLFormField implementation that handle the field. In the mayority of the cases it's obvious when a field is selected (ex: a UISegmentedControl in a field) 29 | @param form The TLFormView instance being setup 30 | @param field The TLFormField instance that fire the event 31 | */ 32 | - (void)formView:(TLFormView *)form didSelectField:(TLFormField *)field; 33 | 34 | /** 35 | @abstract In edit mode, called when the user change the value of some field. 36 | @discussion When this method is fired depends of the TLFormField implementation that handle the field. In the mayority of the cases it's obvious when a value change (ex: a UISegmentedControl in a field) 37 | @param form The TLFormView instance being setup 38 | @param field The TLFormField instance that fire the event 39 | @param value The new value of the field 40 | */ 41 | - (void)formView:(TLFormView *)form didChangeValueForField:(TLFormField *)field newValue:(id)value; 42 | 43 | @end 44 | 45 | 46 | /** 47 | @abstract Data source to describe the setup process of the form. 48 | @discussion This protocol delegates the setup process of the TLFormView to another object. 49 | */ 50 | @protocol TLFormViewDataSource 51 | 52 | @optional 53 | 54 | /** 55 | @abstract Return the actual values to show in each field 56 | @discussion If this method is not implemented the values are allways the ones set as default. Keep in mind that if this method is omited the calls to 'refreshValues' made to TLFromView will have no effect 57 | @param form The TLFormView instance being setup 58 | @param fieldName The name of the field 59 | @return The value corresponding the field name 60 | */ 61 | - (id)formView:(TLFormView *)form valueForFieldWithName:(NSString *)fieldName; 62 | 63 | @required 64 | 65 | /** 66 | @abstract Return an array of NSStrings corresponding to the 'fieldName' of the TLFormField. 67 | @param form The TLFormView instance being setup 68 | @return An NSArray of field names 69 | */ 70 | - (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form; 71 | 72 | /** 73 | @abstract A TLFormField for a given field name 74 | @discussion Return a TLFormField sublass instance for a given field name. 75 | @param form The TLFormView instance being setup 76 | @param fieldName The name of the field 77 | @return The TLFormField object for a given 'fieldName' 78 | */ 79 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName; 80 | 81 | //Return an array of NSStrings. Each string is a constraint to be applied to the form using the to 'constraintsWithVisualFormat:' method of NSLayoutConstrin with the only 82 | //difference that instead of use the view names it should have the field names. e.g: 83 | // 84 | // TLFormField *userNameField = [TLFormField formFieldWithType:TLFormFieldTypeSingleLine name:@"userName" displayName:@"User Name" andDefaultValue:nil]; 85 | // 86 | // [form addConstraintsWithFormat:@[@"V:|-padding-[userName]-padding-|"]]; 87 | // 88 | //Because the TLFormField is a subclass of UIScrollView is important to note that the constraints must be absolute in therms of size. For more details 89 | //see the 'addConstraintsWithFormat:' in TLFormView and check the documentation at 90 | //https://developer.apple.com/library/ios/documentation/userexperience/conceptual/AutolayoutPG/AutoLayoutbyExample/AutoLayoutbyExample.html#//apple_ref/doc/uid/TP40010853-CH5-SW2 91 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form; 92 | 93 | @end 94 | 95 | /************************************************************************************************************************************************/ 96 | /************************************************************** TLFormViewModel ***************************************************************/ 97 | /************************************************************************************************************************************************/ 98 | 99 | /** 100 | @abstract The form model interface 101 | @discussion Describe the iterface that an object must implement to be set as a form model with 'setFormModel:' method of TLFormView. 102 | */ 103 | @protocol TLFormViewModel 104 | @end 105 | 106 | /************************************************************************************************************************************************/ 107 | /**************************************************************** TLFormView ******************************************************************/ 108 | /************************************************************************************************************************************************/ 109 | 110 | 111 | /** 112 | @abstract The form view 113 | @discussion The form view is a subclass of UIScrollView that position his subviews using autolayout constraints. The constraints must be provided by the user of the view. The TLFormView provide means of interaction with his fields throuht a delegate. 114 | The setup is performed by calling the 'setupFields' method or in the first call to 'layoutSubviews'. Multiple calls to this method are allowed, each call remove all the previous setup and start over. The values in the form can be refreshed using the 'refreshValues' method and can be read using the 'valuesForFields' method that returns a NSDictionary with the 'fieldName' property as key. 115 | */ 116 | @interface TLFormView : UIScrollView 117 | 118 | ///This is the default margin used in the format strings of that define the layout. If changed after the layout was applied has no efect. 119 | @property (nonatomic, assign) CGFloat margin; 120 | 121 | ////Set the editing state on all the fields. 122 | @property (nonatomic, assign) BOOL editing; 123 | 124 | ////Delegate that recieve a message every time the user interact with a field that's report interaction 125 | @property (nonatomic, weak) IBOutlet id formDelegate; 126 | 127 | ////Data source 128 | @property (nonatomic, weak) IBOutlet id formDataSource; 129 | 130 | ///Header to show in the form 131 | @property (nonatomic, weak) IBOutlet UIView *header; 132 | ///Footer to show in the form 133 | @property (nonatomic, weak) IBOutlet UIView *footer; 134 | 135 | /** 136 | @abstract Set the form model 137 | @discussion Set an object to act as the form model. A form model is an object that will be set as the *unique* data source and a *shadow* delegate. 138 | 139 | Being a *unique* data source means that when this mehod is call any previous object set as data source will be forgot and any try to set it affter will throw an exception. 140 | 141 | A *shadow* delegate means that other delegate can be set and the messages will be send to both objects, the form model and any other object. 142 | 143 | @param model An object conforming to the TLFormViewModel protocol 144 | */ 145 | - (void)setFormModel:(id )model; 146 | 147 | /** 148 | @abstract Setup the form. 149 | @discussion Make the appropiet calls to the 'formDataSource' and construct the form. 150 | */ 151 | - (void)setupFields; 152 | 153 | /** 154 | @abstract Read the values for all the fields in the form. 155 | @discussion The returned dicionary has the 'fieldName' property of the fields as key and a value of the coresponding type depending on the field type set when created. 156 | @return A NSDictionary containing all the values of the form. 157 | */ 158 | - (NSDictionary *)valuesForFields; 159 | 160 | /** 161 | @abstract Refresh the values in the fields. 162 | @discussion Get the values for each field sending a 'formView:valueForFieldWithName:' to the data source to get the new values and set it on each field. 163 | */ 164 | - (void)reloadValues; 165 | 166 | @end 167 | -------------------------------------------------------------------------------- /Pod/Classes/TLFormView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLFormView.m 3 | // GT 4 | // 5 | // Created by Bruno Berisso on 3/19/14. 6 | // Copyright (c) 2014 Gathered Table LLC. All rights reserved. 7 | // 8 | 9 | #import "TLFormView.h" 10 | #import "TLFormField+Protected.h" 11 | 12 | 13 | @interface TLFormView () 14 | @end 15 | 16 | 17 | @implementation TLFormView { 18 | NSMutableDictionary *viewFieldMap; 19 | UIView *containerView; 20 | NSMutableArray *fieldsWithVisibilityPredicate; 21 | BOOL needsReload; 22 | TLFormField *selectedField; 23 | UIEdgeInsets defaultInsets; 24 | id formModel; 25 | } 26 | 27 | #pragma mark Initialization 28 | 29 | - (id)initWithFrame:(CGRect)frame { 30 | self = [super initWithFrame:frame]; 31 | 32 | if (self) 33 | [self privateInit]; 34 | 35 | return self; 36 | } 37 | 38 | - (void)awakeFromNib { 39 | [super awakeFromNib]; 40 | [self privateInit]; 41 | } 42 | 43 | - (void)privateInit { 44 | //Map the field names with the corresponding views. Used to resolv the autolayout format strings 45 | viewFieldMap = [NSMutableDictionary dictionary]; 46 | 47 | //Fire an automaric reload on the view first display. 48 | needsReload = YES; 49 | 50 | //Handle the keyboard presentation 51 | NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; 52 | 53 | [defaultCenter removeObserver:self]; 54 | [defaultCenter addObserver:self selector:@selector(handleKeyboardShow:) name:UIKeyboardDidShowNotification object:nil]; 55 | [defaultCenter addObserver:self selector:@selector(handleKeyboardHide:) name:UIKeyboardDidHideNotification object:nil]; 56 | [defaultCenter addObserver:self selector:@selector(orientationChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; 57 | 58 | //Dismiss the keyboard on scroll 59 | self.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; 60 | 61 | //This view is used to control how the fields are layed out inside the form. We need to do it this way because of how the scroll view 62 | //manage their content and how the scroll is implemented. (See: http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/index.html 63 | //and http://www.g8production.com/post/57513133020/auto-layout-with-uiscrollview-how-to-use ) 64 | 65 | containerView = [[UIView alloc] init]; 66 | containerView.translatesAutoresizingMaskIntoConstraints = NO; 67 | [self addSubview:containerView]; 68 | 69 | //Set the margin to a default value 70 | self.margin = 8.0; 71 | 72 | #ifdef TLFormViewLayoutDebug 73 | self.backgroundColor = [UIColor redColor]; 74 | containerView.backgroundColor = [UIColor blueColor]; 75 | #endif 76 | } 77 | 78 | - (void)dealloc { 79 | //Remove from notification center on dealloc 80 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 81 | } 82 | 83 | - (void)layoutSubviews { 84 | [super layoutSubviews]; 85 | 86 | if (needsReload) { 87 | needsReload = NO; 88 | [self performSelectorOnMainThread:@selector(setupFields) withObject:nil waitUntilDone:NO]; 89 | } 90 | } 91 | 92 | - (void)setFormDataSource:(id)formDataSource { 93 | if (formModel) 94 | [NSException raise:@"Inconsisten form setup" format:@"A previous data source was set with a form model (setFromModel:). Can't decide which one should prevail."]; 95 | else 96 | _formDataSource = formDataSource; 97 | } 98 | 99 | #pragma mark - TLFormFieldDelegate 100 | 101 | - (void)setFormModel:(id)model { 102 | self.formDataSource = model; 103 | formModel = model; 104 | } 105 | 106 | - (void)didSelectField:(TLFormField *)field { 107 | 108 | //Remember the selected field for handling the keyboard 109 | selectedField = field; 110 | 111 | if ([self.formDelegate respondsToSelector:@selector(formView:didSelectField:)]) 112 | [self.formDelegate formView:self didSelectField:field]; 113 | 114 | if ([formModel respondsToSelector:@selector(formView:didSelectField:)]) 115 | [formModel formView:self didSelectField:field]; 116 | } 117 | 118 | - (void)didChangeValueForField:(TLFormField *)field newValue:(id)value { 119 | if ([self.formDelegate respondsToSelector:@selector(formView:didChangeValueForField:newValue:)]) 120 | [self.formDelegate formView:self didChangeValueForField:field newValue:value]; 121 | 122 | if ([formModel respondsToSelector:@selector(formView:didChangeValueForField:newValue:)]) 123 | [formModel formView:self didChangeValueForField:field newValue:value]; 124 | 125 | [self updateFieldsVisibility]; 126 | } 127 | 128 | #pragma mark - Form fields setup 129 | 130 | - (void)setupFields { 131 | 132 | //Remove any previous layout definition 133 | [containerView removeConstraints:containerView.constraints]; 134 | //and remove all subviews 135 | [[containerView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; 136 | 137 | //Reset the list of fields with variable visibility 138 | fieldsWithVisibilityPredicate = [NSMutableArray array]; 139 | 140 | //Ask the data source for the field names 141 | for (NSString *fieldName in [self.formDataSource fieldNamesToShowInFormView:self]) { 142 | 143 | //for every field name ask for the TLFormField 144 | TLFormField *field = [self.formDataSource formView:self fieldForName:fieldName]; 145 | 146 | //Setup the field internal state 147 | [field setupField:self.editing]; 148 | 149 | [self addField:field]; 150 | } 151 | 152 | //Once all fields are added ask for the layout rules and apply it to the container view 153 | NSArray *layout = [self.formDataSource constraintsFormatForFieldsInForm:self]; 154 | [self setupLayoutWithConstraints:layout]; 155 | 156 | //Ask the datasource for the actual values to show 157 | [self reloadValues]; 158 | } 159 | 160 | - (void)addField:(TLFormField *)field { 161 | //Set ourself as the field delegate 162 | field.formDelegate = self; 163 | //update the view-fieldName map 164 | [viewFieldMap setValue:field forKey:field.fieldName]; 165 | //add the field to the view tree 166 | [containerView addSubview:field]; 167 | 168 | //Register the field if has variable visibility 169 | if (field.visibilityPredicate) 170 | [fieldsWithVisibilityPredicate addObject:field]; 171 | } 172 | 173 | #pragma mark - Layout Setup 174 | 175 | - (void)adjustAuxiliarViewInternalLayout:(UIView *)auxView { 176 | 177 | auxView.translatesAutoresizingMaskIntoConstraints = NO; 178 | 179 | NSMutableDictionary *views = [NSMutableDictionary dictionary]; 180 | NSMutableString *vConstraints = [NSMutableString stringWithString:@"V:|-"]; 181 | NSMutableArray *hConstraints = [NSMutableArray array]; 182 | 183 | //Setup a set of default constraints, arrange the subviews centered in a column. This ensure that the height of the scroll can be calculated 184 | 185 | for (int i = 0; i < auxView.subviews.count; i++) { 186 | NSString *key = [NSString stringWithFormat:@"subView%d", i]; 187 | UIView *subView = auxView.subviews[i]; 188 | 189 | subView.translatesAutoresizingMaskIntoConstraints = NO; 190 | [views setObject:subView forKey:key]; 191 | 192 | [vConstraints appendFormat:@"margin-[%@]-", key]; 193 | [hConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subView(==auxView)]|" 194 | options:0 195 | metrics:nil 196 | views:NSDictionaryOfVariableBindings(subView, auxView)]]; 197 | } 198 | 199 | [vConstraints appendString:@"margin-|"]; 200 | 201 | //Add the vertical and horizontal constraints 202 | [auxView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraints 203 | options:0 204 | metrics:@{@"margin": @(self.margin)} 205 | views:views]]; 206 | [auxView addConstraints:hConstraints]; 207 | } 208 | 209 | - (void)setupLayoutWithConstraints:(NSArray *)constraintFomats { 210 | 211 | //This make the margin available in the rules 212 | NSDictionary *defaultMetrics = @{@"margin": @(self.margin)}; 213 | 214 | for (NSString *formatString in constraintFomats) { 215 | 216 | //Remove all the spaces 217 | NSString *constraintFormat = [formatString stringByReplacingOccurrencesOfString:@" " withString:@""]; 218 | if (constraintFormat.length > 0) { 219 | 220 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintFormat 221 | options:0 222 | metrics:defaultMetrics 223 | views:viewFieldMap]; 224 | [containerView addConstraints:constraints]; 225 | } 226 | } 227 | 228 | //Add constraints for the content view to adjust the size to the form 229 | NSMutableDictionary *views = [NSMutableDictionary dictionaryWithObjects:@[containerView, self] forKeys:@[@"containerView", @"self"]]; 230 | 231 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[containerView(==self)]|" 232 | options:0 233 | metrics:0 234 | views:views]]; 235 | 236 | //Add the header and footer 237 | if (self.header) { 238 | [self adjustAuxiliarViewInternalLayout:self.footer]; 239 | [self addSubview:self.header]; 240 | 241 | [views setObject:self.header forKey:@"header"]; 242 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[header(==self)]|" 243 | options:0 244 | metrics:0 245 | views:views]]; 246 | } 247 | 248 | if (self.footer) { 249 | [self adjustAuxiliarViewInternalLayout:self.footer]; 250 | [self addSubview:self.footer]; 251 | 252 | [views setObject:self.footer forKey:@"footer"]; 253 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[footer(==self)]|" 254 | options:0 255 | metrics:0 256 | views:views]]; 257 | } 258 | 259 | NSString *verticalConstraintFormat = [NSString stringWithFormat:@"V:|%@[containerView]%@|", 260 | self.header ? @"[header]" : @"", 261 | self.footer ? @"[footer]" : @""]; 262 | 263 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalConstraintFormat 264 | options:0 265 | metrics:0 266 | views:views]]; 267 | } 268 | 269 | #pragma mark - Field Values Management 270 | 271 | - (NSDictionary *)valuesForFields { 272 | NSMutableDictionary *values = [NSMutableDictionary dictionary]; 273 | 274 | for (TLFormField *field in [viewFieldMap allValues]) { 275 | id value = [field getValue]; 276 | if (value) 277 | [values setObject:value forKey:field.fieldName]; 278 | } 279 | 280 | return values; 281 | } 282 | 283 | - (void)updateFieldsVisibility { 284 | for (TLFormField *field in fieldsWithVisibilityPredicate) { 285 | BOOL isVisible = [field.visibilityPredicate evaluateWithObject:field substitutionVariables:viewFieldMap]; 286 | field.hidden = !isVisible; 287 | } 288 | } 289 | 290 | - (void)reloadValues { 291 | 292 | if ([self.formDataSource respondsToSelector:@selector(formView:valueForFieldWithName:)]) { 293 | for (TLFormField *field in [viewFieldMap allValues]) { 294 | id value = [self.formDataSource formView:self valueForFieldWithName:field.fieldName]; 295 | [field setValue:value]; 296 | } 297 | 298 | [self updateFieldsVisibility]; 299 | } 300 | } 301 | 302 | #pragma mark - Keyboard Handling 303 | 304 | - (void)handleKeyboardShow:(NSNotification *)notification { 305 | 306 | //If the keyboard is apprearing for any reason other than our fields ignore it 307 | if (!selectedField) 308 | return; 309 | 310 | NSDictionary *info = [notification userInfo]; 311 | CGSize keyboardSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; 312 | 313 | if (UIEdgeInsetsEqualToEdgeInsets(defaultInsets, UIEdgeInsetsZero)) 314 | defaultInsets = self.contentInset; 315 | 316 | UIEdgeInsets contentInsets = self.contentInset; 317 | contentInsets.bottom = keyboardSize.height; 318 | 319 | self.contentInset = contentInsets; 320 | self.scrollIndicatorInsets = contentInsets; 321 | 322 | CGRect aRect = self.frame; 323 | aRect.size.height -= keyboardSize.height; 324 | 325 | CGRect fieldFrame = selectedField.frame; 326 | CGPoint fieldBottomRight = CGPointMake(fieldFrame.origin.x + fieldFrame.size.width, fieldFrame.origin.y + fieldFrame.size.height); 327 | 328 | if (!CGRectContainsPoint(aRect, fieldBottomRight)) 329 | [self scrollRectToVisible:fieldFrame animated:NO]; 330 | } 331 | 332 | - (void)handleKeyboardHide:(NSNotification *)notification { 333 | self.contentInset = defaultInsets; 334 | self.scrollIndicatorInsets = defaultInsets; 335 | } 336 | 337 | - (void)orientationChange:(NSNotification *)notification { 338 | [self endEditing:YES]; 339 | } 340 | 341 | @end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLFormView 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/TLFormView.svg?style=flat)](http://cocoadocs.org/docsets/TLFormView) 4 | [![License](https://img.shields.io/cocoapods/l/TLFormView.svg?style=flat)](http://cocoadocs.org/docsets/TLFormView) 5 | [![Platform](https://img.shields.io/cocoapods/p/TLFormView.svg?style=flat)](http://cocoadocs.org/docsets/TLFormView) 6 | 7 | TLFormView is _yet another_ form view *truly* universal. This means that the same component support both iPhone and iPad using a mechanism around the [Auto Layout Visual Format] to adjust the layout to the running device. 8 | 9 | Because it doesn't extend ``UITableView`` you are completely free to create anything to use as a form field as long as it extends the base ``TLFormField`` class. It also has some nice features like: conditional visibility using ``NSPredicate``, in-place help for each field with ``UIPopoverControler`` and on-the-fly edit/read-only modes switch among other things. 10 | 11 | There is a serie of blog posts called: _TLFormView: the form of the future_ [part I] and [part II] at the [Tryolabs Blog]. 12 | 13 | #### Quick Example 14 | 15 | ```objective-c 16 | 17 | //Declare your data model as an extension of TLFormModel 18 | 19 | @interface UserModel : TLFormModel 20 | 21 | @property (nonatomic, strong) TLFormImage *avatar; 22 | @property (nonatomic, strong) TLFormText *user_name; 23 | @property (nonatomic, strong) TLFormNumber *age; 24 | 25 | @end 26 | 27 | @implementation UserModel @end 28 | 29 | ... 30 | 31 | @implementation SomeViewController 32 | 33 | - (void)viewDidLoad { 34 | [super viewDidLoad]; 35 | 36 | FormUserModel *formUserModel = [FormUserModel new]; 37 | 38 | //This copy the values of our user model to the form model constructing the correct types using plain C functions 39 | formUserModel.avatar = TLFormImage(userModel.avatar); 40 | formUserModel.user_name = TLFormText(userModel.userName); 41 | formUserModel.age = TLFormNumber(userModel.age); 42 | 43 | //'form' is an instance of TLFormView. 44 | [self.form setFormModel:formUserModel]; 45 | self.form.editing = YES; 46 | } 47 | 48 | ... 49 | 50 | - (void)saveAction:(id)sender { 51 | //Read the values entered by the user and save it to disc. 52 | NSDictionary values = @{ 53 | @"avatar": formUserModel.avatar, 54 | @"user_name": formUserModel.user_name, 55 | @"age": formUserModel.age 56 | } 57 | [values writeToURL:[self saveUrl] atomically:YES]; 58 | } 59 | 60 | ``` 61 | 62 | This is how it will look: 63 | 64 |

65 | 66 |

67 | 68 | ## Usage 69 | 70 | There are two ways to setup ``TLFormView``: 71 | 72 | - The short one showed in the example above. Using [TLFormModel](#form-setup) class is simple and easy but is less flexible if you are planning to use it _as is_. 73 | - And the long one. Is more powerfull and give you full control over how the form will behave in his life cycle (e.g., when update the fields, who the values are formated, etc). 74 | 75 | Eventualy you can start using ``TLFormModel`` and extend your sublcass overriding the right methods to gain the level of control that the _long way_ gives you. I first will talk about the traditional setup to also explain how ``TLFormView`` works and then I will discuss ``TLFormModel`` in more detail. 76 | 77 | #### Form Setup: the long way 78 | 79 | There is two basic components: ``TLFormView`` and ``TLFormField``. ``TLFormView`` inherit from UIScrollView and add another data source and delegate to create and handle events form the form. You need to implement three methods of the ``TLFormViewDataSource`` protocol to get the form functional, these are: 80 | 81 | ```objective-c 82 | - (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form; 83 | 84 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName; 85 | 86 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form; 87 | ``` 88 | 89 | ``fieldNamesToShowInFormView:`` return an array of strings containing the _field names_ (or ids) used to identify the fields in the form. ``formView:fieldForName:`` creates a field for every field name in the form. A field is any subclass of ``TLFormField`` (that is a sub class of ``UIView``), there is a set of default fields that can be used to fit the 80% of the use case. These are: 90 | 91 | - ``TLFormFieldImage``: for display images form an url or a raw image 92 | - ``TLFormFieldList``: for display a list of things 93 | - ``TLFormFieldMultiLine``: to show long text 94 | - ``TLFormFieldSingleLine``: to show short text, numbers and bool values 95 | - ``TLFormFieldTitle``: to show a short text formatted as a title 96 | 97 | The project has a category over ``TLFormField`` with basic methods for customising the aspect of the fields. It has some methods to configure the title and give access to the ``UIAppearance`` proxy of the internal components. 98 | 99 | Once you give a ``TLFormField`` for each field name to the form you need to define how to layout those fields. To do so you need to implement the third method of the data source ``constraintsFormatForFieldsInForm:`` and return an array of strings containing the the rules in [Auto Layout Visual Format] to place the fields in the screen. When writing the rules you need to reference the fields using the field names you return in the ``fieldNamesToShowInFormView:`` implementation. Here you have the chance to check for the capabilities of the device and adapt your layout changing the rules as you need. 100 | 101 | Here is an example implementation of the three required methods in ``TLFormViewDataSource``: 102 | 103 | ```objective-c 104 | - (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form { 105 | return @[ 106 | @"user_name", 107 | @"avatar", 108 | @"age", 109 | ]; 110 | } 111 | 112 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName { 113 | 114 | Class fieldClass; 115 | NSString *title; 116 | id value; 117 | 118 | if ([fieldName isEqualToString:@"user_name"]) { 119 | fieldClass = [TLFormFieldSingleLine class]; 120 | title = @"User Name"; 121 | value = userModel.name; 122 | } 123 | else if ([fieldName isEqualToString:@"avatar"]) { 124 | fieldClass = [TLFormFieldImage class]; 125 | title = @"Avatar"; 126 | value = userModel.avatarUrl; 127 | } 128 | else { 129 | fieldClass = [TLFormFieldSingleLine class]; 130 | title = @"Age"; 131 | value = userModel.age; 132 | } 133 | 134 | return [fieldClass formFieldWithName:fieldName title:title andDefaultValue:value]; 135 | } 136 | 137 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form { 138 | 139 | //For iPhone we want a vertical layout like we get on a UITableView 140 | 141 | if (isIPhone) { 142 | return @[ 143 | //Place the avatar on the top 144 | @"V:|-[avatar(==230)]-", 145 | @"H:|-[avatar(==420)]-|", 146 | 147 | //Now place all the fields to the bottom 148 | @"V:[avatar]-[user_name(>=44)]-", 149 | @"H:|-[user_name]-|", 150 | 151 | @"V:[age(==user_name)]-|", 152 | @"H:|-[age]-|" 153 | ]; 154 | 155 | //For anything else we will place the image on the top left and the rest of the fields to the right 156 | } else { 157 | return @[ 158 | 159 | //Place the avatar on the top left 160 | @"V:|-[avatar(==230)]", 161 | @"H:|-[avatar]", 162 | 163 | //Now place all the fields to the right 164 | @"V:|-[user_name(>=44)]", 165 | @"H:|-[avatar]-[user_name]-|", 166 | 167 | @"V:[user_name]-[age(==user_name)]-|", 168 | @"H:|-[avatar(==420)]-[age]-|" 169 | ]; 170 | } 171 | } 172 | ``` 173 | 174 | This example produce this on iPhone 175 | 176 |

177 | 178 |

179 | 180 | and this on iPad 181 | 182 |

183 | 184 |

185 | 186 | ###### Conditional Visibility 187 | 188 | There are cases when you need to show a particular field only when other fields has a special value. For this propose ``TLFormField`` has a property ``visibilityPredicate`` that you can set to an ``NSPredicate`` at the time the field is created. The predicate can access the values of all the fields in the form through the [NSPredicate variable substitution]. This means that in you can access any field value in the form with the syntax ``$[name of the field].value``. 189 | 190 | Let suppose that we want to show the "User Name" field only if the "Age" is greater than 12 years old. We can write a predicate that evaluate to true if the "age" field is greater than 12 and assign it as the ``visibilityPredicate`` for the "user_name" field. This is the code: 191 | 192 | ```objective-c 193 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName { 194 | ... 195 | //Asume that 'field' is an instance of TLFormField created before somehow 196 | if ([fieldName isEqualToString:@"user_name"]) 197 | field.visibilityPredicate = [NSPredicate predicateWithFormat:@"$age.value > 12"]; 198 | ... 199 | } 200 | ``` 201 | 202 | ###### In-place Help 203 | 204 | Suppose that we want to explain the strange behaviour of our previous example in a concise way. To explain this kind of little special behaviours we can use the in-place support provided by ``TLFormField``. The only thing we need to do is set the help text string in the ``helpText`` property in the field. When there is a value for that property the field show a question mark icon next to the field title that when is taped show the help text in a popup window. Here is the code: 205 | 206 | ```objective-c 207 | - (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName { 208 | ... 209 | if ([fieldName isEqualToString:@"age"]) 210 | field.helpText = @"This is the age of the user. Users with less than 12 years are not allowed to enter his name."; 211 | ... 212 | } 213 | ``` 214 | 215 | And this is how it looks when the icon is taped: 216 | 217 |

218 | 219 |

220 | 221 | #### TLFormModel: the short way 222 | 223 | To help you with the setup of the form there is a class ``TLFormModel`` that do what we just did automatically inferring the implementation of the ``TLFormViewDataSource`` form his own taxonomy. You only need to extend it and add one property for each field you want to show in the form. The types of the properties must be one of the types declared in the file TLFormModel.h. Each type corresponds to one of the standard ``TLFormField`` provided. 224 | 225 | To read the values from the form just access the properties like you wold do on any object. To write the values you will need to wrap the values in one of the value constructors provide for each type. Once the values in the model are updated you need to manually perform a reload to show the new values with the ``reloadValues`` method of ``TLFormView`` (this is a cheep update, no view destruction is involved). 226 | 227 | These are the declarations for the types supported by ``TLFormModel``: 228 | - ``TLFormSeparator``: extend NSObject. Rendered as a separator in the form, allow to group fields in sections. Has no value or title. 229 | - ``TLFormText``: extend NSString. 230 | - ``TLFormLongText``: extend NSString. 231 | - ``TLFormTitle``: extend NSString. 232 | - ``TLFormNumber``: extend NSNumber. 233 | - ``TLFormBoolean``: extend NSNumber. 234 | - ``TLFormList``: extend NSArray. 235 | - ``TLFormEnumerated``: extend NSDictionary. 236 | - ``TLFormImage``: extend NSObject. 237 | 238 | These classes doesn't have any logic other than the one inherited form his superclasses, they act almost as an annotation over a property. The intended way to construct values of this types is with plain C functions declared in the TLFormModel.h that check the type of the parameter and copy the value given as parameter. 239 | 240 | Using the type and value of his properties ``TLFormModel`` infer what kind of form field should use for each property. For the field title the property name is used. If the property name use snake-case naming convention each "_" is translated to a space and all words are capitalised, so the property "user_name" will have the title "User Name". The order of the fields is the one used to declare the properties. For ex: 241 | 242 | ```objective-c 243 | @interface UserModel : TLFormModel 244 | 245 | @property (nonatomic, strong) TLFormImage *avatar; 246 | @property (nonatomic, strong) TLFormText *user_name; 247 | 248 | @end 249 | ``` 250 | 251 | Will present the avatar field at the top and the user name below. 252 | 253 | Our previous example could be written with ``TLFormModel`` like this: 254 | 255 | ```objective-c 256 | @interface UserModel : TLFormModel 257 | 258 | @property (nonatomic, strong) TLFormImage *avatar; 259 | @property (nonatomic, strong) TLFormText *user_name; 260 | @property (nonatomic, strong) TLFormNumber *age; 261 | 262 | @end 263 | 264 | @implementation UserModel @end 265 | ``` 266 | 267 | That's it. This will produce a vertical layout like the one we get on iPhone on all the platforms. Now to connect the ``TLFormModel`` to the form we need to use the ``setFormModel:`` method on the ``TLFormView`` we are using. 268 | 269 | ```objective-c 270 | ... 271 | 272 | //At some point in some place... 273 | FormUserModel *formUserModel = [FormUserModel new]; 274 | 275 | //This copy the values of our user model to the form model constructing the correct types using plain C functions 276 | formUserModel.avatar = TLFormImage(userModel.avatar); 277 | formUserModel.user_name = TLFormText(userModel.userName); 278 | formUserModel.age = TLFormNumber(userModel.age); 279 | 280 | TLFormView *form = ... 281 | [form setFormModel:formUserModel]; 282 | ``` 283 | 284 | To adapt the layout for other device families we need to override the implementation of ``constraintsFormatForFieldsInForm:`` provided by ``TLFormModel`` like this: 285 | 286 | ```objective-c 287 | @implementation UserModel 288 | 289 | - (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form { 290 | 291 | //For iPhone we want the default implementation provided so just return the 'super' version 292 | if (isIPhone) 293 | return [super constraintsFormatForFieldsInForm:form]; 294 | 295 | //For anything else use our custom layout 296 | else { 297 | return @[ 298 | 299 | //Place the avatar on the top left 300 | @"V:|-[avatar(==230)]", 301 | @"H:|-[avatar]", 302 | 303 | //Now place all the fields to the right 304 | @"V:|-[user_name(>=44)]", 305 | @"H:|-[avatar]-[user_name]-|", 306 | 307 | @"V:[user_name]-[age(==user_name)]-|", 308 | @"H:|-[avatar(==420)]-[age]-|" 309 | ]; 310 | } 311 | } 312 | 313 | @end 314 | ``` 315 | 316 | Now suppose that we want to edit this user info and get the result back. We need to toggle the edit state on the form and read the updated values from the ``TLFormModel`` instance we are using. This is how we could do it: 317 | 318 | ```objective-c 319 | 320 | - (IBAction)toggleEditionAction:(id)sender { 321 | //Set the form on edit mode 322 | self.form.editing = !self.form.editing; 323 | //update the fields to reflect it 324 | [self.form setupFields]; 325 | } 326 | 327 | - (IBAction)saveUserAction:(id)sender { 328 | //Read the values entered by the user and save it to disc. 329 | 330 | NSDictionary values = @{ 331 | @"avatar": formUserModel.avatar, 332 | @"user_name": formUserModel.name, 333 | @"age": formUserModel.age 334 | } 335 | [values writeToURL:[self saveUrl] atomically:YES]; 336 | } 337 | 338 | ``` 339 | 340 | About "editing" the images. The default ``TLFormFieldImage`` don't provide any way to pick an image at this point. You will need to handle the field selection and show some kind of image picker. 341 | 342 | #### Event handling 343 | 344 | ``TLFormView`` report two events through ``TLFormViewDelegate`` out of the box: ``didSelectField:`` and ``didChangeValueForField:``. The events are fired by a ``TLFormField`` implementation that notify a form about it and the form then propagate the event out. Depending on how the field is implemented it might have more events to report. For example the ``TLFormFieldList`` use a ``UITableView`` to present the list of values so it has it's own delegate that allows to customise if the rows can be rearranged, or the selection of one row. 345 | 346 | ## Requirements 347 | 348 | iOS >= 8.0 349 | 350 | ## Installation 351 | 352 | TLFormView is available through [CocoaPods](http://cocoapods.org). To install 353 | it, simply add the following line to your Podfile: 354 | 355 | pod "TLFormView" 356 | 357 | Or clone the repo and check the code under `Pod/Classes`. 358 | 359 | ## Todo 360 | 361 | There are many things that need improvement, here are some: 362 | 363 | - [ ] Centralised style using the methods on ``TLFormField+UIAppearance.h`` 364 | - [ ] Expose the ‘defaultMetrics’ as part of the style 365 | - [ ] TLFormModel: look a better way to make objects mutations more efficient 366 | - [ ] Add keyboard next/prev buttons 367 | - [ ] Validate with predicate 368 | - [ ] Refactor the TLFormView to TLFormViewController so we can automatically handle: choose fotos, add a string to a list, layout for orientation, etc. 369 | 370 | If you want to contribute to the project please consider pick one of this items. 371 | 372 | ## Author 373 | 374 | BrunoBerisso, bruno@tryolabs.com 375 | 376 | ## License 377 | 378 | TLFormView is available under the MIT license. See the LICENSE file for more info. 379 | 380 | [Auto Layout Visual Format]: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage/VisualFormatLanguage.html#//apple_ref/doc/uid/TP40010853-CH3-SW11 381 | [NSPredicate variable substitution]: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Predicates/Articles/pCreating.html 382 | [part I]: http://blog.tryolabs.com/2015/05/14/tlformview-the-form-of-the-future-is-here-part-i/ 383 | [part II]: http://blog.tryolabs.com/2015/05/20/tlformview-the-form-of-the-future-is-here-part-ii/ 384 | [Tryolabs Blog]: http://blog.tryolabs.com/ 385 | -------------------------------------------------------------------------------- /Screenshots/in-place_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Screenshots/in-place_help.png -------------------------------------------------------------------------------- /Screenshots/ipad_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Screenshots/ipad_ex_1.png -------------------------------------------------------------------------------- /Screenshots/iphone_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Screenshots/iphone_ex_1.png -------------------------------------------------------------------------------- /Screenshots/iphone_quick_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLFormView/0678ae37a8cb83179bde95e4b9da49100c4f7e45/Screenshots/iphone_quick_ex.png -------------------------------------------------------------------------------- /TLFormView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint TLFormView.podspec' to ensure this is a 3 | # valid spec and remove all comments before submitting the spec. 4 | # 5 | # Any lines starting with a # are optional, but encouraged 6 | # 7 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 8 | # 9 | 10 | Pod::Spec.new do |s| 11 | s.name = "TLFormView" 12 | s.version = "0.0.5" 13 | s.summary = "An universal iOS form" 14 | s.homepage = "https://github.com/tryolabs/TLFormView" 15 | s.license = 'MIT' 16 | s.author = { "BrunoBerisso" => "bruno@tryolabs.com" } 17 | s.source = { :git => "https://github.com/tryolabs/TLFormView.git", :tag => s.version.to_s } 18 | 19 | s.platform = :ios, '8.0' 20 | s.requires_arc = true 21 | 22 | s.source_files = 'Pod/Classes/**/*' 23 | end 24 | --------------------------------------------------------------------------------