├── .gitignore ├── GSForm.podspec ├── GSForm ├── GSForm.h └── sources │ ├── GSFormBuilder.h │ ├── GSFormBuilder.m │ ├── GSFormViewController.h │ ├── GSFormViewController.m │ ├── GSRow.h │ ├── GSRow.m │ ├── GSSection.h │ └── GSSection.m ├── GSFormDev ├── GSFormDev.xcodeproj │ └── project.pbxproj └── GSFormDev │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── GSLabelFieldCell.h │ ├── GSLabelFieldCell.m │ ├── GenderPickerViewController.h │ ├── GenderPickerViewController.m │ ├── Info.plist │ ├── RegisterViewController.h │ ├── RegisterViewController.m │ ├── StepperCell.h │ ├── StepperCell.m │ ├── ViewController.h │ ├── ViewController.m │ └── main.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | *.xcworkspace 20 | !default.xcworkspace 21 | *.xcuserdata -------------------------------------------------------------------------------- /GSForm.podspec: -------------------------------------------------------------------------------- 1 | # 引用命令 pod 'GSForm', :svn =>"https://192.168.1.9/svn/IOS/otherprojects/GSKit/GSForm" 2 | 3 | 4 | Pod::Spec.new do |s| 5 | 6 | s.name = "GSForm" 7 | 8 | s.version = "3.0" 9 | s.summary = "this is a GSForm." 10 | 11 | s.description = <<-DESC 12 | just use it! 13 | DESC 14 | s.homepage = "http://www.souhuow.com" 15 | 16 | s.license = "MIT" 17 | 18 | s.author = { "Brook" => "cirpxpp@163.com"} 19 | 20 | s.platform = :ios, "8.0" 21 | 22 | s.source = { :svn => "https://192.168.1.9/svn/IOS/otherprojects/GSKit/GSForm", :tag => "#{s.version}" } 23 | 24 | s.source_files = 'GSForm/*.{h,m}' 25 | 26 | end 27 | -------------------------------------------------------------------------------- /GSForm/GSForm.h: -------------------------------------------------------------------------------- 1 | // 2 | // GSForm.h 3 | // GSFormDev 4 | // 5 | // Created by beforeold on 2022/8/25. 6 | // 7 | 8 | #import 9 | 10 | #import "GSRow.h" 11 | #import "GSSection.h" 12 | #import "GSFormBuilder.h" 13 | #import "GSFormViewController.h" 14 | -------------------------------------------------------------------------------- /GSForm/sources/GSFormBuilder.h: -------------------------------------------------------------------------------- 1 | // 整个 tableView 的数据源对象 2 | 3 | #import 4 | 5 | @class GSSection; 6 | @class GSRow; 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @interface GSFormBuilder : NSObject 11 | 12 | @property (nonatomic, strong, readonly) NSMutableArray *sectionArray; 13 | @property (nonatomic, assign, readonly) NSUInteger count; 14 | 15 | @property (nonatomic, assign) CGFloat rowHeight; 16 | 17 | - (void)addSection:(GSSection *)section; 18 | - (void)removeSection:(GSSection *)section; 19 | 20 | - (void)reformRespRet:(id)resp; 21 | - (id)fetchHttpParams; 22 | 23 | - (NSDictionary *)validateRows; 24 | 25 | /// 配置全局禁用点击事件的block 26 | @property (nonatomic, copy) id(^disableBlock)(GSFormBuilder *); 27 | 28 | /// 根据 indexPath 返回 row 29 | - (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath; 30 | /// 根据 row 返回 indexPath 31 | - (NSIndexPath *)indexPathOfGSRow:(GSRow *)row; 32 | 33 | @end 34 | 35 | @interface GSFormBuilder (NSSubscript) 36 | 37 | // 数组样式 38 | - (GSSection *)objectAtIndexedSubscript:(NSUInteger)idx ; // 取值 39 | - (void)setObject:(GSSection *)obj atIndexedSubscript:(NSUInteger)idx ; // 设值 40 | 41 | @end 42 | 43 | NS_ASSUME_NONNULL_END 44 | -------------------------------------------------------------------------------- /GSForm/sources/GSFormBuilder.m: -------------------------------------------------------------------------------- 1 | #import "GSFormBuilder.h" 2 | #import "GSSection.h" 3 | 4 | @interface GSFormBuilder () 5 | 6 | @property (nonatomic, strong, readwrite) NSMutableArray *sectionArray; 7 | 8 | @end 9 | 10 | @implementation GSFormBuilder 11 | - (void)addSection:(GSSection *)section { 12 | [self.sectionArray addObject:section]; 13 | } 14 | 15 | - (void)removeSection:(GSSection *)section { 16 | [self.sectionArray removeObject:section]; 17 | } 18 | 19 | - (void)reformRespRet:(id)resp { 20 | for (GSSection *section in self.sectionArray) { 21 | for (GSRow *row in section.rowArray) { 22 | !row.reformRespRetBlock ?: row.reformRespRetBlock(resp, row.value); 23 | } 24 | } 25 | } 26 | 27 | - (id)fetchHttpParams { 28 | NSMutableDictionary *dic = [NSMutableDictionary dictionary]; 29 | for (GSSection *secion in self.sectionArray) { 30 | for (GSRow *row in secion.rowArray) { 31 | if (!row.httpParamConfigBlock) continue; 32 | id http = row.httpParamConfigBlock(row.value); 33 | if ([http isKindOfClass:[NSDictionary class]]) { 34 | [dic addEntriesFromDictionary:http]; 35 | } else if ([http isKindOfClass:[NSArray class]]) { 36 | for (NSDictionary *subHttp in http) { 37 | [dic addEntriesFromDictionary:subHttp]; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return dic; 44 | } 45 | 46 | - (NSDictionary *)validateRows { 47 | for (GSSection *section in self.sectionArray) { 48 | for (GSRow *row in section.rowArray) { 49 | if (!row.isHidden && row.valueValidateBlock) { 50 | NSDictionary *dic = row.valueValidateBlock(row.value); 51 | NSNumber *ret = dic[kValidateRetKey]; 52 | NSAssert(ret, @"必须有结果参数"); 53 | if (!ret) continue; 54 | if (!ret.boolValue) return dic; 55 | } 56 | } 57 | } 58 | 59 | return rowOK(); 60 | } 61 | 62 | - (NSIndexPath *)indexPathOfGSRow:(GSRow *)row { 63 | if (row.isHidden) return nil; 64 | if (!row.section || row.section.hidden) return nil; 65 | 66 | GSSection *xSection = row.section; 67 | NSInteger sectionCounter = -1; 68 | BOOL matchSection = NO; 69 | for (GSSection *section in self.sectionArray) { 70 | if(!section.isHidden) sectionCounter ++; 71 | if (section == xSection) { 72 | matchSection = YES; 73 | break; 74 | } 75 | } 76 | if(!matchSection) return nil; 77 | 78 | NSInteger rowCounter = -1; 79 | BOOL matchRow = NO; 80 | for (GSRow *rowL in xSection.rowArray) { 81 | if(!rowL.isHidden) rowCounter ++; 82 | if (rowL == row) { 83 | matchRow = YES; 84 | break; 85 | } 86 | } 87 | if(!matchRow) return nil; 88 | 89 | return [NSIndexPath indexPathForRow:rowCounter inSection:sectionCounter]; 90 | } 91 | 92 | - (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath { 93 | NSInteger sectionCounter = -1; 94 | GSSection *xSection = nil; 95 | for (GSSection *section in self.sectionArray) { 96 | if (!section.isHidden) sectionCounter ++; 97 | if (sectionCounter == indexPath.section) { 98 | xSection = section; 99 | break; 100 | } 101 | } 102 | 103 | if (sectionCounter == -1) return nil; 104 | 105 | NSInteger rowCounter = -1; 106 | for (GSRow *row in xSection.rowArray) { 107 | if(!row.isHidden) rowCounter++; 108 | if(rowCounter == indexPath.row) { 109 | return row; 110 | } 111 | } 112 | 113 | return nil; 114 | } 115 | 116 | #pragma mark - setter/getter 117 | - (NSMutableArray *)sectionArray { 118 | if (_sectionArray) return _sectionArray; 119 | 120 | _sectionArray = [NSMutableArray array]; 121 | return _sectionArray; 122 | } 123 | 124 | - (NSUInteger)count { 125 | return self.sectionArray.count; 126 | } 127 | 128 | @end 129 | 130 | @implementation GSFormBuilder (NSSubscript) 131 | // 数组样式 132 | - (GSSection *)objectAtIndexedSubscript:(NSUInteger)idx { 133 | if (self.sectionArray.count > idx) return self.sectionArray[idx]; 134 | 135 | return nil; 136 | } 137 | 138 | - (void)setObject:(GSSection *)obj atIndexedSubscript:(NSUInteger)idx { 139 | if (self.sectionArray.count < idx) return; 140 | self.sectionArray[idx] = obj; 141 | } 142 | 143 | @end 144 | -------------------------------------------------------------------------------- /GSForm/sources/GSFormViewController.h: -------------------------------------------------------------------------------- 1 | // 表单控制器的基类 2 | 3 | #import 4 | 5 | @class GSFormBuilder; 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface GSFormViewController : UIViewController 10 | 11 | @property (nonatomic, strong) UITableView *tableView; 12 | @property (nonatomic, strong) GSFormBuilder *form; 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /GSForm/sources/GSFormViewController.m: -------------------------------------------------------------------------------- 1 | // controllers 2 | #import "GSFormViewController.h" 3 | #import "GSFormBuilder.h" 4 | #import "GSSection.h" 5 | 6 | // views 7 | 8 | 9 | @interface UITableViewCell (AddLine) 10 | 11 | /** 12 | * 给UITableViewCell添加上划线下划线 在UITableViewDelegate的代理方法 - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 调用 13 | * 14 | * @param isTop 是否显示上划线 15 | * @param isBottom 是否显示下划线 16 | */ 17 | - (void)gs_updateCellLine:(BOOL)isTop isBottom:(BOOL)isBottom; 18 | 19 | - (void)gs_updateEnable:(NSIndexPath *)indexPath target:(id)target action:(SEL)action enable:(BOOL)enable; 20 | 21 | @end 22 | 23 | @interface GSFormViewController () 24 | 25 | @end 26 | 27 | @implementation GSFormViewController 28 | 29 | #pragma mark - lifeCycle 30 | - (void)viewDidLoad 31 | { 32 | [super viewDidLoad]; 33 | 34 | [self normalSetup]; 35 | [self configureSubview]; 36 | } 37 | 38 | - (void)viewDidLayoutSubviews 39 | { 40 | [super viewDidLayoutSubviews]; 41 | 42 | self.tableView.frame = self.view.bounds; 43 | } 44 | 45 | #pragma mark - event response 46 | /// 处理当 row 不可用时的点击响应 47 | - (void)disableClick:(UIButton *)button { 48 | UITableViewCell *cell = (UITableViewCell *)[button superview]; 49 | NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; 50 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 51 | 52 | /// 当自身进行不可用判断时优先返回 53 | if (row.disableValidateBlock) { 54 | NSNumber *number = row.disableValidateBlock(row.value, NO)[kValidateRetKey]; 55 | BOOL enable = number.boolValue; 56 | if (!enable) { 57 | row.disableValidateBlock(row.value, YES); 58 | return; 59 | } 60 | } 61 | 62 | // 如果全局判断为 disable 63 | if (self.form.disableBlock) { 64 | self.form.disableBlock(self.form); 65 | } 66 | } 67 | 68 | #pragma mark - protocol 69 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 70 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 71 | 72 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.reuseIdentifier]; 73 | if (!cell) { 74 | if (row.cellClass) { 75 | cell = [[row.cellClass alloc] initWithStyle:row.style reuseIdentifier:row.reuseIdentifier]; 76 | } else { 77 | cell = [[[NSBundle mainBundle] loadNibNamed:row.nibName owner:nil options:nil] lastObject]; 78 | } 79 | 80 | !row.cellExtraInitBlock ?: row.cellExtraInitBlock(cell, row.value, indexPath); 81 | } 82 | 83 | NSAssert(!(row.rowConfigBlockWithCompletion && row.rowConfigBlock), @"row config block 二选一"); 84 | 85 | GSRowConfigCompletion completion = nil; 86 | if (row.rowConfigBlock) { 87 | row.rowConfigBlock(cell, row.value, indexPath); 88 | 89 | } else if (row.rowConfigBlockWithCompletion) { 90 | completion = row.rowConfigBlockWithCompletion(cell, row.value, indexPath); 91 | } 92 | 93 | [self handleEnableForCell:cell gsRow:row atIndexPath:indexPath]; 94 | 95 | !completion ?: completion(); 96 | 97 | return cell; 98 | } 99 | 100 | - (void)tableView:(UITableView *)tableView 101 | willDisplayCell:(UITableViewCell *)cell 102 | forRowAtIndexPath:(NSIndexPath *)indexPath 103 | { 104 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 105 | 106 | [cell gs_updateCellLine:row.hasTopSep isBottom:row.hasBottomSep]; 107 | } 108 | 109 | - (CGFloat)tableView:(UITableView *)tableView 110 | heightForRowAtIndexPath:(NSIndexPath *)indexPath { 111 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 112 | 113 | return row.rowHeight == 0 ? UITableViewAutomaticDimension : row.rowHeight; 114 | } 115 | 116 | - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { 117 | 118 | return [self.form[section] headerHeight]; 119 | } 120 | 121 | - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { 122 | return [self.form[section] footerHeight]; 123 | } 124 | 125 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 126 | NSInteger count = 0; 127 | for (GSSection *section in self.form.sectionArray) { 128 | if(!section.isHidden) count++; 129 | } 130 | 131 | return count; 132 | } 133 | 134 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 135 | GSSection *fSection = self.form[section]; 136 | NSInteger count = 0; 137 | for (GSRow *row in fSection.rowArray) { 138 | if(!row.isHidden) count++; 139 | } 140 | 141 | return count; 142 | } 143 | 144 | - (void)tableView:(UITableView *)tableView 145 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 146 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 147 | 148 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 149 | !row.didSelectBlock ?: row.didSelectBlock(indexPath, row.value); 150 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; 151 | !row.didSelectCellBlock ?: row.didSelectCellBlock(indexPath, row.value, cell); 152 | } 153 | 154 | #pragma mark - private methods 155 | // 初始化控制器的一些初始参数、状态等 156 | - (void)normalSetup 157 | { 158 | self.view.backgroundColor = [UIColor whiteColor]; 159 | } 160 | 161 | // 配置视图 162 | - (void)configureSubview 163 | { 164 | [self.view addSubview:self.tableView]; 165 | } 166 | 167 | - (void)handleEnableForCell:(UITableViewCell *)cell gsRow:(GSRow *)row atIndexPath:(NSIndexPath *)indexPath { 168 | BOOL enable = YES; 169 | 170 | if (self.form.disableBlock) { // 如果全局禁用 171 | enable = NO; 172 | if (row.enableValidateBlock) { 173 | NSNumber *number = row.enableValidateBlock(row.value, NO)[kValidateRetKey]; 174 | enable = number.boolValue; 175 | } 176 | } else if (row.disableValidateBlock) { 177 | NSNumber *number = row.disableValidateBlock(row.value, NO)[kValidateRetKey]; 178 | enable = number.boolValue; 179 | } 180 | 181 | [cell gs_updateEnable:indexPath target:self action:@selector(disableClick:) enable:enable]; 182 | } 183 | 184 | #pragma mark - getters/setters 185 | - (UITableView *)tableView { 186 | if (!_tableView) { 187 | _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; 188 | _tableView.delegate = self; 189 | _tableView.dataSource = self; 190 | _tableView.backgroundColor = [UIColor groupTableViewBackgroundColor]; 191 | _tableView.tableFooterView = [[UIView alloc] init]; 192 | _tableView.rowHeight = UITableViewAutomaticDimension; 193 | _tableView.estimatedRowHeight = 88.f; 194 | } 195 | 196 | return _tableView; 197 | } 198 | 199 | - (GSFormBuilder *)form { 200 | if (!_form) { 201 | _form = [[GSFormBuilder alloc] init]; 202 | } 203 | 204 | return _form; 205 | } 206 | 207 | @end 208 | 209 | @implementation UITableViewCell (GSAddLine) 210 | 211 | #define kTopLineViewTag 200001 212 | #define kBottomLineViewTag 200002 213 | 214 | /// 考虑到2x/3x屏幕的像素值设置 215 | #define kScreenScale [[UIScreen mainScreen] scale] 216 | #define PIXEL_INTEGRAL(pointValue) (round(pointValue * kScreenScale) / kScreenScale) 217 | #define kSeparatorLineWidth PIXEL_INTEGRAL(1) 218 | 219 | - (void)gs_updateCellLine:(BOOL)isTop isBottom:(BOOL)isBottom { 220 | UILabel *topLine = [self viewWithTag:kTopLineViewTag]; 221 | UILabel *bottomLine = [self viewWithTag:kBottomLineViewTag]; 222 | 223 | if (!topLine) { 224 | UIColor *color = [UIColor colorWithRed:0.929412 green:0.929412 blue:0.929412 alpha:1]; 225 | topLine = [[UILabel alloc] init]; 226 | topLine.backgroundColor = color; 227 | topLine.tag = kTopLineViewTag; 228 | } 229 | [self.contentView addSubview:topLine]; 230 | 231 | if (!bottomLine) { 232 | UIColor *color = [UIColor colorWithRed:0.929412 green:0.929412 blue:0.929412 alpha:1]; 233 | bottomLine = [[UILabel alloc] init]; 234 | bottomLine.backgroundColor = color; 235 | bottomLine.tag = kBottomLineViewTag; 236 | } 237 | [self.contentView addSubview:bottomLine]; 238 | 239 | CGFloat offset = 13; 240 | topLine.frame = CGRectMake(0, 0, self.frame.size.width, kSeparatorLineWidth); 241 | bottomLine.frame = CGRectMake(offset, self.frame.size.height - kSeparatorLineWidth, self.frame.size.width - offset, kSeparatorLineWidth); 242 | topLine.hidden = !isTop; 243 | bottomLine.hidden = !isBottom; 244 | } 245 | 246 | - (void)gs_updateEnable:(NSIndexPath *)indexPath target:(id)target action:(SEL)action enable:(BOOL)enable { 247 | NSInteger tag = 7876434; 248 | UIButton *button = [self viewWithTag:tag]; 249 | if (!button) { 250 | button = [[UIButton alloc] initWithFrame:self.bounds]; 251 | button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 252 | button.tag = tag; 253 | [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside]; 254 | [self addSubview:button]; 255 | } 256 | 257 | button.userInteractionEnabled = !enable; 258 | } 259 | 260 | @end 261 | -------------------------------------------------------------------------------- /GSForm/sources/GSRow.h: -------------------------------------------------------------------------------- 1 | // 描述 row 的数据源 2 | 3 | #import 4 | #import 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | extern NSString *kGSHTTPPropertyKey; 9 | extern NSString *kGSHTTPValueKey; 10 | 11 | extern NSString *kValidateRetKey; 12 | extern NSString *kValidateMsgKey; 13 | 14 | 15 | typedef void(^GSRowConfigCompletion)(void); 16 | 17 | /// 18 | static inline NSDictionary *rowError(NSString *msg) { 19 | return @{kValidateMsgKey: msg ?: @"", 20 | kValidateRetKey:@NO}; 21 | } 22 | 23 | static inline NSDictionary *rowOK(void) { 24 | return @{kValidateRetKey:@YES}; 25 | } 26 | 27 | @class GSSection; 28 | @interface GSRow : NSObject 29 | 30 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 31 | reuseIdentifier:(NSString *)reuseIdentifier; 32 | 33 | @property (nonatomic, assign, readonly) UITableViewCellStyle style; 34 | @property (nonatomic, copy, readonly) NSString *reuseIdentifier; 35 | @property (nonatomic, strong) Class cellClass; 36 | @property (nonatomic, copy) NSString *nibName; 37 | @property (nonatomic, assign) CGFloat rowHeight; 38 | @property (nonatomic, strong) id value; 39 | @property (nonatomic, copy) NSString *noValueDisplayText; 40 | @property (nonatomic, assign) BOOL hasTopSep; 41 | @property (nonatomic, assign) BOOL hasBottomSep; 42 | @property (nonatomic, assign, getter=isHidden) BOOL hidden; 43 | 44 | /* 45 | * 下面两个二选一 46 | */ 47 | 48 | @property (nullable, nonatomic, copy) void(^rowConfigBlock)(id cell, id value, NSIndexPath *indexPath); // cellForRow 49 | @property (nullable, nonatomic, copy) GSRowConfigCompletion(^rowConfigBlockWithCompletion)(id cell, id value, NSIndexPath *indexPath); // final row config at cellForRow 50 | 51 | @property (nullable, nonatomic, copy) void(^cellExtraInitBlock)(id cell, id value, NSIndexPath *indexPath); // if(!cell) { extraInitBlock }; 52 | 53 | /// check isValid 54 | @property (nullable, nonatomic, copy) NSDictionary *(^valueValidateBlock)(id value); 55 | @property (nullable, nonatomic, copy) void(^didSelectBlock)(NSIndexPath *indexPath, id value); // didSelectRow 56 | @property (nullable, nonatomic, copy) void(^didSelectCellBlock)(NSIndexPath *indexPath, id value, id cell); // didSelectRow with Cell 57 | @property (nullable, nonatomic, copy) void(^reformRespRetBlock)(id ret, id value); // 外部传值处理 58 | @property (nullable, nonatomic, copy) id(^httpParamConfigBlock)(id value); // get param for http request 59 | 60 | /// 判断是否【启用】 row 的条件 block,如果返回YES,则 cell 激活,返回NO,则会被禁用 61 | /// 配合 GSForm 的全局 disable 一起使用 62 | /// didSelect 变量是 判断该block的调用是否为点击事件的调用 63 | @property (nullable, nonatomic, copy) NSDictionary *(^enableValidateBlock)(id value, BOOL didClick); 64 | 65 | /// 判断是否【禁用】 row 的条件block,如果返回YES,则 cell 激活,返回NO,则会被禁用 66 | /// 当全局的 disable 存在时,此 block 不执行 67 | /// didSelect 变量是 判断该block的调用是否为点击事件的调用 68 | @property (nullable, nonatomic, copy) NSDictionary *(^disableValidateBlock)(id value, BOOL didClick); 69 | 70 | /// 指向所在 section 71 | @property (nullable, nonatomic, weak) GSSection *section; 72 | 73 | @end 74 | 75 | NS_ASSUME_NONNULL_END 76 | 77 | /* 78 | * 因为控制器会持有row对象 79 | * 注意循环引用,在block内部调用 section 或者 控制器时使用弱引用 80 | * 由于在 cellConfigBlock 内有调用 cell 的回调 delegate 的可能,因此需要在 block 内部进行再一次的 weak 处理 81 | * 示例见下方 82 | */ 83 | 84 | /* 85 | 86 | WEAK_SELF 87 | row.cellConfigBlock = ^(GSTCodeScanCell *cell, id value) { 88 | STRONG_SELF 89 | 90 | __weak typeof(cell) weakCell = cell; 91 | __weak typeof(strongSelf) weakWeakSelf = strongSelf; 92 | cell.scanClickBlock = ^(){ 93 | GSQRCodeController *scanVC = [[GSQRCodeController alloc] init]; 94 | scanVC.returnScanBarCodeValue = ^(NSString *str) { 95 | value[kCellRightContent] = str; 96 | NSIndexPath *indexPath = [weakWeakSelf.tableView indexPathForCell:weakCell]; 97 | [weakWeakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; 98 | }; 99 | [weakWeakSelf.navigationController pushViewController:scanVC animated:YES]; 100 | }; 101 | }; 102 | 103 | */ 104 | 105 | -------------------------------------------------------------------------------- /GSForm/sources/GSRow.m: -------------------------------------------------------------------------------- 1 | #import "GSRow.h" 2 | 3 | NSString *kGSHTTPPropertyKey = @"kGSHTTPPropertyKey"; 4 | NSString *kGSHTTPValueKey = @"kGSHTTPValueKey"; 5 | 6 | NSString *kValidateRetKey = @"kValidateRetKey"; 7 | NSString *kValidateMsgKey = @"kValidateMsgKey"; 8 | 9 | @interface GSRow () 10 | 11 | @end 12 | 13 | @implementation GSRow 14 | 15 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 16 | self = [super init]; 17 | if (self) { 18 | _style = style; 19 | _reuseIdentifier = [reuseIdentifier copy]; 20 | } 21 | 22 | return self; 23 | } 24 | 25 | #ifdef DEBUG 26 | - (void)dealloc { 27 | NSLog(@"row dealloc %@", self); 28 | } 29 | #endif 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /GSForm/sources/GSSection.h: -------------------------------------------------------------------------------- 1 | // 描述 section 的数据源 2 | 3 | #import 4 | #import "GSRow.h" 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | @interface GSSection : NSObject 9 | 10 | @property (nonatomic, strong, readonly) NSMutableArray *rowArray; 11 | @property (nonatomic, assign, readonly) NSUInteger count; 12 | @property (nonatomic, assign) CGFloat headerHeight; 13 | @property (nonatomic, assign) CGFloat footerHeight; 14 | @property (nonatomic, assign, getter=isHidden) BOOL hidden; 15 | 16 | - (void)addRow:(GSRow *)row; 17 | - (void)addRowArray:(NSArray *)rowArray; 18 | 19 | @end 20 | 21 | @interface GSSection (NSSubscript) 22 | 23 | // 数组样式 24 | - (GSRow *)objectAtIndexedSubscript:(NSUInteger)idx ; // 取值 25 | - (void)setObject:(GSRow *)obj atIndexedSubscript:(NSUInteger)idx ; // 设值 26 | 27 | @end 28 | 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /GSForm/sources/GSSection.m: -------------------------------------------------------------------------------- 1 | #import "GSSection.h" 2 | 3 | @interface GSSection () 4 | 5 | @property (nonatomic, strong, readwrite) NSMutableArray *rowArray; 6 | 7 | @end 8 | 9 | @implementation GSSection 10 | - (void)addRow:(GSRow *)row { 11 | row.section = self; 12 | [self.rowArray addObject:row]; 13 | } 14 | 15 | - (void)addRowArray:(NSArray *)rowArray { 16 | for (GSRow *row in rowArray) { 17 | [self addRow:row]; 18 | } 19 | } 20 | 21 | - (CGFloat)footerHeight { 22 | if (_footerHeight != 0) return _footerHeight; 23 | 24 | return 0.01; 25 | } 26 | 27 | - (CGFloat)headerHeight { 28 | if (_headerHeight != 0) return _headerHeight; 29 | 30 | return 0.01; 31 | } 32 | 33 | #pragma mark - setter/getter 34 | - (NSMutableArray *)rowArray { 35 | if (_rowArray) return _rowArray; 36 | 37 | _rowArray = [NSMutableArray array]; 38 | return _rowArray; 39 | } 40 | 41 | - (NSUInteger)count { 42 | return self.rowArray.count; 43 | } 44 | 45 | @end 46 | 47 | @implementation GSSection (NSSubscript) 48 | // 数组样式 49 | - (GSRow *)objectAtIndexedSubscript:(NSUInteger)idx { 50 | if (self.rowArray.count > idx) return self.rowArray[idx]; 51 | 52 | return nil; 53 | } 54 | 55 | - (void)setObject:(GSRow *)obj atIndexedSubscript:(NSUInteger)idx { 56 | if (self.rowArray.count < idx) return; 57 | self.rowArray[idx] = obj; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BCC743731EBC53C200AA449C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC743721EBC53C200AA449C /* main.m */; }; 11 | BCC743761EBC53C200AA449C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC743751EBC53C200AA449C /* AppDelegate.m */; }; 12 | BCC743791EBC53C200AA449C /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC743781EBC53C200AA449C /* ViewController.m */; }; 13 | BCC7437C1EBC53C200AA449C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BCC7437A1EBC53C200AA449C /* Main.storyboard */; }; 14 | BCC7437E1EBC53C200AA449C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BCC7437D1EBC53C200AA449C /* Assets.xcassets */; }; 15 | BCC743811EBC53C200AA449C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BCC7437F1EBC53C200AA449C /* LaunchScreen.storyboard */; }; 16 | BCC7439E1EBC553C00AA449C /* GSFormBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC743971EBC553C00AA449C /* GSFormBuilder.m */; }; 17 | BCC7439F1EBC553C00AA449C /* GSFormViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC743991EBC553C00AA449C /* GSFormViewController.m */; }; 18 | BCC743A01EBC553C00AA449C /* GSRow.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC7439B1EBC553C00AA449C /* GSRow.m */; }; 19 | BCC743A11EBC553C00AA449C /* GSSection.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC7439D1EBC553C00AA449C /* GSSection.m */; }; 20 | BCC97A371EF1A63200FAEE25 /* RegisterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC97A361EF1A63200FAEE25 /* RegisterViewController.m */; }; 21 | BCC97A3A1EF1A82700FAEE25 /* GSLabelFieldCell.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC97A391EF1A82700FAEE25 /* GSLabelFieldCell.m */; }; 22 | BCC97A3D1EF1A9B200FAEE25 /* StepperCell.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC97A3C1EF1A9B200FAEE25 /* StepperCell.m */; }; 23 | BCC97A401EF1AFA700FAEE25 /* GenderPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC97A3F1EF1AFA700FAEE25 /* GenderPickerViewController.m */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 94C2845A28B6926000DA9C4E /* GSForm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSForm.h; sourceTree = ""; }; 28 | BCC7436E1EBC53C100AA449C /* GSFormDev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GSFormDev.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | BCC743721EBC53C200AA449C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 30 | BCC743741EBC53C200AA449C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 31 | BCC743751EBC53C200AA449C /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 32 | BCC743771EBC53C200AA449C /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 33 | BCC743781EBC53C200AA449C /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 34 | BCC7437B1EBC53C200AA449C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 35 | BCC7437D1EBC53C200AA449C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | BCC743801EBC53C200AA449C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 37 | BCC743821EBC53C200AA449C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | BCC743961EBC553C00AA449C /* GSFormBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSFormBuilder.h; sourceTree = ""; }; 39 | BCC743971EBC553C00AA449C /* GSFormBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSFormBuilder.m; sourceTree = ""; }; 40 | BCC743981EBC553C00AA449C /* GSFormViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSFormViewController.h; sourceTree = ""; }; 41 | BCC743991EBC553C00AA449C /* GSFormViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSFormViewController.m; sourceTree = ""; }; 42 | BCC7439A1EBC553C00AA449C /* GSRow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSRow.h; sourceTree = ""; }; 43 | BCC7439B1EBC553C00AA449C /* GSRow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSRow.m; sourceTree = ""; }; 44 | BCC7439C1EBC553C00AA449C /* GSSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSSection.h; sourceTree = ""; }; 45 | BCC7439D1EBC553C00AA449C /* GSSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSSection.m; sourceTree = ""; }; 46 | BCC97A351EF1A63200FAEE25 /* RegisterViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegisterViewController.h; sourceTree = ""; }; 47 | BCC97A361EF1A63200FAEE25 /* RegisterViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegisterViewController.m; sourceTree = ""; }; 48 | BCC97A381EF1A82700FAEE25 /* GSLabelFieldCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSLabelFieldCell.h; sourceTree = ""; }; 49 | BCC97A391EF1A82700FAEE25 /* GSLabelFieldCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSLabelFieldCell.m; sourceTree = ""; }; 50 | BCC97A3B1EF1A9B200FAEE25 /* StepperCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StepperCell.h; sourceTree = ""; }; 51 | BCC97A3C1EF1A9B200FAEE25 /* StepperCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StepperCell.m; sourceTree = ""; }; 52 | BCC97A3E1EF1AFA700FAEE25 /* GenderPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GenderPickerViewController.h; sourceTree = ""; }; 53 | BCC97A3F1EF1AFA700FAEE25 /* GenderPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GenderPickerViewController.m; sourceTree = ""; }; 54 | /* End PBXFileReference section */ 55 | 56 | /* Begin PBXFrameworksBuildPhase section */ 57 | BCC7436B1EBC53C100AA449C /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 94C2845B28B692A900DA9C4E /* sources */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | BCC7439A1EBC553C00AA449C /* GSRow.h */, 71 | BCC7439B1EBC553C00AA449C /* GSRow.m */, 72 | BCC7439C1EBC553C00AA449C /* GSSection.h */, 73 | BCC7439D1EBC553C00AA449C /* GSSection.m */, 74 | BCC743961EBC553C00AA449C /* GSFormBuilder.h */, 75 | BCC743971EBC553C00AA449C /* GSFormBuilder.m */, 76 | BCC743981EBC553C00AA449C /* GSFormViewController.h */, 77 | BCC743991EBC553C00AA449C /* GSFormViewController.m */, 78 | ); 79 | path = sources; 80 | sourceTree = ""; 81 | }; 82 | BCC743651EBC53C100AA449C = { 83 | isa = PBXGroup; 84 | children = ( 85 | BCC743951EBC553C00AA449C /* GSForm */, 86 | BCC743701EBC53C100AA449C /* GSFormDev */, 87 | BCC7436F1EBC53C100AA449C /* Products */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | BCC7436F1EBC53C100AA449C /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | BCC7436E1EBC53C100AA449C /* GSFormDev.app */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | BCC743701EBC53C100AA449C /* GSFormDev */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | BCC97A3B1EF1A9B200FAEE25 /* StepperCell.h */, 103 | BCC97A3C1EF1A9B200FAEE25 /* StepperCell.m */, 104 | BCC97A381EF1A82700FAEE25 /* GSLabelFieldCell.h */, 105 | BCC97A391EF1A82700FAEE25 /* GSLabelFieldCell.m */, 106 | BCC743741EBC53C200AA449C /* AppDelegate.h */, 107 | BCC743751EBC53C200AA449C /* AppDelegate.m */, 108 | BCC743771EBC53C200AA449C /* ViewController.h */, 109 | BCC743781EBC53C200AA449C /* ViewController.m */, 110 | BCC97A351EF1A63200FAEE25 /* RegisterViewController.h */, 111 | BCC97A361EF1A63200FAEE25 /* RegisterViewController.m */, 112 | BCC97A3E1EF1AFA700FAEE25 /* GenderPickerViewController.h */, 113 | BCC97A3F1EF1AFA700FAEE25 /* GenderPickerViewController.m */, 114 | BCC7437A1EBC53C200AA449C /* Main.storyboard */, 115 | BCC7437D1EBC53C200AA449C /* Assets.xcassets */, 116 | BCC7437F1EBC53C200AA449C /* LaunchScreen.storyboard */, 117 | BCC743821EBC53C200AA449C /* Info.plist */, 118 | BCC743711EBC53C200AA449C /* Supporting Files */, 119 | ); 120 | path = GSFormDev; 121 | sourceTree = ""; 122 | }; 123 | BCC743711EBC53C200AA449C /* Supporting Files */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | BCC743721EBC53C200AA449C /* main.m */, 127 | ); 128 | name = "Supporting Files"; 129 | sourceTree = ""; 130 | }; 131 | BCC743951EBC553C00AA449C /* GSForm */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 94C2845A28B6926000DA9C4E /* GSForm.h */, 135 | 94C2845B28B692A900DA9C4E /* sources */, 136 | ); 137 | name = GSForm; 138 | path = ../GSForm; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | BCC7436D1EBC53C100AA449C /* GSFormDev */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = BCC743851EBC53C200AA449C /* Build configuration list for PBXNativeTarget "GSFormDev" */; 147 | buildPhases = ( 148 | BCC7436A1EBC53C100AA449C /* Sources */, 149 | BCC7436B1EBC53C100AA449C /* Frameworks */, 150 | BCC7436C1EBC53C100AA449C /* Resources */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = GSFormDev; 157 | productName = GSFormDev; 158 | productReference = BCC7436E1EBC53C100AA449C /* GSFormDev.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | /* End PBXNativeTarget section */ 162 | 163 | /* Begin PBXProject section */ 164 | BCC743661EBC53C100AA449C /* Project object */ = { 165 | isa = PBXProject; 166 | attributes = { 167 | BuildIndependentTargetsInParallel = YES; 168 | CLASSPREFIX = GS; 169 | LastUpgradeCheck = 1540; 170 | ORGANIZATIONNAME = ""; 171 | TargetAttributes = { 172 | BCC7436D1EBC53C100AA449C = { 173 | CreatedOnToolsVersion = 8.2.1; 174 | ProvisioningStyle = Automatic; 175 | }; 176 | }; 177 | }; 178 | buildConfigurationList = BCC743691EBC53C100AA449C /* Build configuration list for PBXProject "GSFormDev" */; 179 | compatibilityVersion = "Xcode 3.2"; 180 | developmentRegion = en; 181 | hasScannedForEncodings = 0; 182 | knownRegions = ( 183 | en, 184 | Base, 185 | ); 186 | mainGroup = BCC743651EBC53C100AA449C; 187 | productRefGroup = BCC7436F1EBC53C100AA449C /* Products */; 188 | projectDirPath = ""; 189 | projectRoot = ""; 190 | targets = ( 191 | BCC7436D1EBC53C100AA449C /* GSFormDev */, 192 | ); 193 | }; 194 | /* End PBXProject section */ 195 | 196 | /* Begin PBXResourcesBuildPhase section */ 197 | BCC7436C1EBC53C100AA449C /* Resources */ = { 198 | isa = PBXResourcesBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | BCC743811EBC53C200AA449C /* LaunchScreen.storyboard in Resources */, 202 | BCC7437E1EBC53C200AA449C /* Assets.xcassets in Resources */, 203 | BCC7437C1EBC53C200AA449C /* Main.storyboard in Resources */, 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | }; 207 | /* End PBXResourcesBuildPhase section */ 208 | 209 | /* Begin PBXSourcesBuildPhase section */ 210 | BCC7436A1EBC53C100AA449C /* Sources */ = { 211 | isa = PBXSourcesBuildPhase; 212 | buildActionMask = 2147483647; 213 | files = ( 214 | BCC743A01EBC553C00AA449C /* GSRow.m in Sources */, 215 | BCC97A401EF1AFA700FAEE25 /* GenderPickerViewController.m in Sources */, 216 | BCC743791EBC53C200AA449C /* ViewController.m in Sources */, 217 | BCC7439E1EBC553C00AA449C /* GSFormBuilder.m in Sources */, 218 | BCC97A371EF1A63200FAEE25 /* RegisterViewController.m in Sources */, 219 | BCC97A3A1EF1A82700FAEE25 /* GSLabelFieldCell.m in Sources */, 220 | BCC97A3D1EF1A9B200FAEE25 /* StepperCell.m in Sources */, 221 | BCC743761EBC53C200AA449C /* AppDelegate.m in Sources */, 222 | BCC7439F1EBC553C00AA449C /* GSFormViewController.m in Sources */, 223 | BCC743731EBC53C200AA449C /* main.m in Sources */, 224 | BCC743A11EBC553C00AA449C /* GSSection.m in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin PBXVariantGroup section */ 231 | BCC7437A1EBC53C200AA449C /* Main.storyboard */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | BCC7437B1EBC53C200AA449C /* Base */, 235 | ); 236 | name = Main.storyboard; 237 | sourceTree = ""; 238 | }; 239 | BCC7437F1EBC53C200AA449C /* LaunchScreen.storyboard */ = { 240 | isa = PBXVariantGroup; 241 | children = ( 242 | BCC743801EBC53C200AA449C /* Base */, 243 | ); 244 | name = LaunchScreen.storyboard; 245 | sourceTree = ""; 246 | }; 247 | /* End PBXVariantGroup section */ 248 | 249 | /* Begin XCBuildConfiguration section */ 250 | BCC743831EBC53C200AA449C /* Debug */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_COMMA = YES; 263 | CLANG_WARN_CONSTANT_CONVERSION = YES; 264 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 265 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 266 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 267 | CLANG_WARN_EMPTY_BODY = YES; 268 | CLANG_WARN_ENUM_CONVERSION = YES; 269 | CLANG_WARN_INFINITE_RECURSION = YES; 270 | CLANG_WARN_INT_CONVERSION = YES; 271 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 273 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 275 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 276 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 277 | CLANG_WARN_STRICT_PROTOTYPES = YES; 278 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = dwarf; 284 | ENABLE_STRICT_OBJC_MSGSEND = YES; 285 | ENABLE_TESTABILITY = YES; 286 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 287 | GCC_C_LANGUAGE_STANDARD = gnu99; 288 | GCC_DYNAMIC_NO_PIC = NO; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_OPTIMIZATION_LEVEL = 0; 291 | GCC_PREPROCESSOR_DEFINITIONS = ( 292 | "DEBUG=1", 293 | "$(inherited)", 294 | ); 295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 297 | GCC_WARN_UNDECLARED_SELECTOR = YES; 298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 299 | GCC_WARN_UNUSED_FUNCTION = YES; 300 | GCC_WARN_UNUSED_VARIABLE = YES; 301 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 302 | MTL_ENABLE_DEBUG_INFO = YES; 303 | ONLY_ACTIVE_ARCH = YES; 304 | SDKROOT = iphoneos; 305 | TARGETED_DEVICE_FAMILY = "1,2"; 306 | }; 307 | name = Debug; 308 | }; 309 | BCC743841EBC53C200AA449C /* Release */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 316 | CLANG_CXX_LIBRARY = "libc++"; 317 | CLANG_ENABLE_MODULES = YES; 318 | CLANG_ENABLE_OBJC_ARC = YES; 319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_COMMA = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 326 | CLANG_WARN_EMPTY_BODY = YES; 327 | CLANG_WARN_ENUM_CONVERSION = YES; 328 | CLANG_WARN_INFINITE_RECURSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 332 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 334 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 335 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 336 | CLANG_WARN_STRICT_PROTOTYPES = YES; 337 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 338 | CLANG_WARN_UNREACHABLE_CODE = YES; 339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 340 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 341 | COPY_PHASE_STRIP = NO; 342 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 343 | ENABLE_NS_ASSERTIONS = NO; 344 | ENABLE_STRICT_OBJC_MSGSEND = YES; 345 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu99; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 355 | MTL_ENABLE_DEBUG_INFO = NO; 356 | SDKROOT = iphoneos; 357 | TARGETED_DEVICE_FAMILY = "1,2"; 358 | VALIDATE_PRODUCT = YES; 359 | }; 360 | name = Release; 361 | }; 362 | BCC743861EBC53C200AA449C /* Debug */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 366 | DEVELOPMENT_TEAM = ""; 367 | INFOPLIST_FILE = GSFormDev/Info.plist; 368 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 369 | LD_RUNPATH_SEARCH_PATHS = ( 370 | "$(inherited)", 371 | "@executable_path/Frameworks", 372 | ); 373 | PRODUCT_BUNDLE_IDENTIFIER = com.bo.gsform; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | }; 376 | name = Debug; 377 | }; 378 | BCC743871EBC53C200AA449C /* Release */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 382 | DEVELOPMENT_TEAM = ""; 383 | INFOPLIST_FILE = GSFormDev/Info.plist; 384 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 385 | LD_RUNPATH_SEARCH_PATHS = ( 386 | "$(inherited)", 387 | "@executable_path/Frameworks", 388 | ); 389 | PRODUCT_BUNDLE_IDENTIFIER = com.bo.gsform; 390 | PRODUCT_NAME = "$(TARGET_NAME)"; 391 | }; 392 | name = Release; 393 | }; 394 | /* End XCBuildConfiguration section */ 395 | 396 | /* Begin XCConfigurationList section */ 397 | BCC743691EBC53C100AA449C /* Build configuration list for PBXProject "GSFormDev" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | BCC743831EBC53C200AA449C /* Debug */, 401 | BCC743841EBC53C200AA449C /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | BCC743851EBC53C200AA449C /* Build configuration list for PBXNativeTarget "GSFormDev" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | BCC743861EBC53C200AA449C /* Debug */, 410 | BCC743871EBC53C200AA449C /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | /* End XCConfigurationList section */ 416 | }; 417 | rootObject = BCC743661EBC53C100AA449C /* Project object */; 418 | } 419 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | 8 | @end 9 | 10 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | //#define GSFormBaseVC GSBaseVCViewController 4 | #import "GSFormViewController.h" 5 | 6 | #define XXX GSBaseVCViewController 7 | 8 | @interface AppDelegate () 9 | 10 | @end 11 | 12 | @implementation AppDelegate 13 | 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 16 | // Override point for customization after application launch. 17 | 18 | GSFormViewController *formVC = [[GSFormViewController alloc] init]; 19 | 20 | NSLog(@"formvc %@", formVC); 21 | 22 | // NSLog(@"xx %s", ""#XXX".h"); 23 | NSString *ret = [NSString stringWithFormat:@"%s.h", "#XXX"]; 24 | NSLog(@"%@", ret); 25 | 26 | return YES; 27 | } 28 | 29 | 30 | - (void)applicationWillResignActive:(UIApplication *)application { 31 | // 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. 32 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 33 | } 34 | 35 | 36 | - (void)applicationDidEnterBackground:(UIApplication *)application { 37 | // 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. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | 42 | - (void)applicationWillEnterForeground:(UIApplication *)application { 43 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | 47 | - (void)applicationDidBecomeActive:(UIApplication *)application { 48 | // 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. 49 | } 50 | 51 | 52 | - (void)applicationWillTerminate:(UIApplication *)application { 53 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 54 | } 55 | 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/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 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/GSLabelFieldCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface GSLabelFieldCell : UITableViewCell 4 | 5 | @property (nonatomic, strong) UILabel *leftlabel; 6 | @property (nonatomic, strong) UITextField *rightField; 7 | 8 | @property (nonatomic, copy) void(^textChangeBlock)(NSString *text); 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/GSLabelFieldCell.m: -------------------------------------------------------------------------------- 1 | #import "GSLabelFieldCell.h" 2 | 3 | static const CGFloat kFieldWidth = 150.f; 4 | 5 | @implementation GSLabelFieldCell 6 | 7 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 8 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 9 | if (self) { 10 | [self.contentView addSubview:self.leftlabel]; 11 | [self.contentView addSubview:self.rightField]; 12 | 13 | } 14 | 15 | return self; 16 | } 17 | 18 | - (void)textFieldDidChange:(UITextField *)field { 19 | if ([self textChangeBlock]) { 20 | [self textChangeBlock](field.text); 21 | } 22 | } 23 | 24 | - (void)layoutSubviews { 25 | [super layoutSubviews]; 26 | 27 | CGFloat margin = 15; 28 | self.leftlabel.frame = CGRectMake(margin, 0, self.contentView.frame.size.width, self.contentView.frame.size.height); 29 | 30 | CGFloat padding = 10; 31 | CGFloat x = self.contentView.frame.size.width - margin - kFieldWidth; 32 | self.rightField.frame = CGRectMake(x, padding, kFieldWidth, self.contentView.frame.size.height - 2 * padding); 33 | } 34 | 35 | #pragma mark - setter/getter 36 | - (void)setTextChangeBlock:(void (^)(NSString *))textChangeBlock { 37 | _textChangeBlock = [textChangeBlock copy]; 38 | [_rightField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; 39 | } 40 | 41 | - (UILabel *)leftlabel { 42 | if (_leftlabel) return _leftlabel; 43 | 44 | _leftlabel = [[UILabel alloc] init]; 45 | _leftlabel.font = [UIFont systemFontOfSize:15]; 46 | _leftlabel.textColor = [UIColor blackColor]; 47 | 48 | return _leftlabel; 49 | } 50 | 51 | - (UITextField *)rightField { 52 | if (!_rightField) { 53 | _rightField = [[UITextField alloc] init]; 54 | _rightField.textAlignment = NSTextAlignmentRight; 55 | _rightField.textColor = [UIColor darkGrayColor]; 56 | _rightField.font = [UIFont systemFontOfSize:13]; 57 | } 58 | 59 | return _rightField; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/GenderPickerViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface GenderPickerViewController : UIViewController 4 | 5 | @property (nonatomic, copy) void(^pickBlock)(BOOL isMale); 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/GenderPickerViewController.m: -------------------------------------------------------------------------------- 1 | #import "GenderPickerViewController.h" 2 | 3 | @interface GenderPickerViewController () 4 | 5 | @end 6 | 7 | @implementation GenderPickerViewController 8 | 9 | - (void)viewDidLoad { 10 | [super viewDidLoad]; 11 | 12 | self.view.backgroundColor = [UIColor whiteColor]; 13 | self.edgesForExtendedLayout = UIRectEdgeNone; 14 | 15 | for (NSInteger i = 0; i < 2; ++i) { 16 | UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; 17 | CGFloat width = 88; 18 | 19 | [button setTitle:i ? @"Male" :@"Female" forState:UIControlStateNormal]; 20 | button.frame = CGRectMake(50 + i * (width + 20), 100, width, 50); 21 | button.tag = i; 22 | [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; 23 | [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside]; 24 | 25 | [self.view addSubview:button]; 26 | } 27 | } 28 | 29 | - (void)buttonClick:(UIButton *)button { 30 | !self.pickBlock ?: self.pickBlock(button.tag); 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/RegisterViewController.h: -------------------------------------------------------------------------------- 1 | #import "GSForm.h" 2 | 3 | @interface RegisterViewController : GSFormViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/RegisterViewController.m: -------------------------------------------------------------------------------- 1 | #import "RegisterViewController.h" 2 | 3 | // view 4 | #import "GSLabelFieldCell.h" 5 | #import "StepperCell.h" 6 | 7 | // controller 8 | #import "GenderPickerViewController.h" 9 | 10 | 11 | static NSString *const kLeftKey = @"kLeftKey"; // 标记左侧内容 12 | static NSString *const kRightKey = @"kRightKey"; // 标记右侧内容 13 | static NSString *const kFlagKey = @"kFlagKey"; // 用于标记是否有箭头 14 | static NSString *const kDisableKey = @"kDisableKey"; // 用于标记禁用 textField 15 | 16 | @interface RegisterViewController () 17 | 18 | @end 19 | 20 | @implementation RegisterViewController 21 | #pragma mark - lifeCycle 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | 25 | [self initialSetup]; 26 | [self buildRows]; 27 | } 28 | 29 | #pragma mark - event reponse 30 | - (void)submit:(id)sender { 31 | NSDictionary *dic = [self.form validateRows]; 32 | 33 | if (![dic[kValidateRetKey] boolValue]) { 34 | [self alertMsg:dic[kValidateMsgKey]]; 35 | } else { 36 | // 获取请求参数,可以根据业务需求,自定义 fetch 方法用于发起请求 37 | // 比如此类中的 fetchParams 方法 38 | NSString *msg = [[self.form fetchHttpParams] description]; 39 | [self alertMsg:msg title:@"Params are OK"]; 40 | } 41 | } 42 | 43 | - (NSDictionary *)fetchParams { 44 | NSMutableDictionary *dic = [NSMutableDictionary dictionary]; 45 | for (GSSection *secion in self.form.sectionArray) { 46 | for (GSRow *row in secion.rowArray) { 47 | if (!row.httpParamConfigBlock) continue; 48 | id http = row.httpParamConfigBlock(row.value); 49 | if ([http isKindOfClass:[NSDictionary class]]) { 50 | [dic addEntriesFromDictionary:http]; 51 | } else if ([http isKindOfClass:[NSArray class]]) { 52 | for (NSDictionary *subHttp in http) { 53 | [dic addEntriesFromDictionary:subHttp]; 54 | } 55 | } 56 | } 57 | } 58 | 59 | return dic; 60 | 61 | } 62 | 63 | 64 | #pragma mark - private methods 65 | - (void)initialSetup { 66 | self.title = @"Registration"; 67 | self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; 68 | self.edgesForExtendedLayout = UIRectEdgeNone; 69 | 70 | UIBarButtonItem *right = [[UIBarButtonItem alloc] initWithTitle:@"Submit" 71 | style:UIBarButtonItemStylePlain 72 | target:self 73 | action:@selector(submit:)]; 74 | self.navigationItem.rightBarButtonItem = right; 75 | } 76 | 77 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 78 | return section ? @"DETAILS" : @"ACCOUNT"; 79 | } 80 | 81 | 82 | - (void)buildRows { 83 | [self.form addSection:[self sectionForAccount]]; 84 | [self.form addSection:[self sectionForDetail]]; 85 | } 86 | 87 | - (GSSection *)sectionForAccount { 88 | GSSection *section = nil; 89 | GSRow *row = nil; 90 | 91 | section = [[GSSection alloc] init]; 92 | section.headerHeight = 40; 93 | 94 | 95 | NSDictionary *dic = @{kLeftKey:@"Email"}; 96 | row = [self rowForFieldWithUserInfo:dic]; 97 | [section addRow:row]; 98 | 99 | dic = @{kLeftKey:@"Password"}; 100 | GSRow *row1 = [self rowForFieldWithUserInfo:dic]; 101 | [section addRow:row1]; 102 | 103 | dic = @{kLeftKey:@"Repeat Password"}; 104 | row = [self rowForFieldWithUserInfo:dic]; 105 | 106 | /// 校验密码是否一致 107 | row.valueValidateBlock = ^NSDictionary *(id value){ 108 | if ([row1.value[kRightKey] isEqualToString:value[kRightKey]]) return rowOK(); 109 | return rowError(@"Two password should be the same"); 110 | }; 111 | 112 | [section addRow:row]; 113 | 114 | return section; 115 | } 116 | 117 | - (GSSection *)sectionForDetail { 118 | GSSection *section = nil; 119 | GSRow *row = nil; 120 | 121 | section = [[GSSection alloc] init]; 122 | section.headerHeight = 40; 123 | 124 | NSDictionary *dic = @{kLeftKey:@"Name"}; 125 | row = [self rowForFieldWithUserInfo:dic]; 126 | [section addRow:row]; 127 | 128 | 129 | row = [self rowForGender]; 130 | [section addRow:row]; 131 | 132 | 133 | row = [self rowForBirthDay]; 134 | [section addRow:row]; 135 | 136 | 137 | row = [self rowForStepper]; 138 | [section addRow:row]; 139 | 140 | return section; 141 | } 142 | 143 | - (GSRow *)rowForGender { 144 | GSRow *row = nil; 145 | 146 | NSDictionary *dic = @{kLeftKey:@"Gender", 147 | kFlagKey:@YES, 148 | kDisableKey:@YES}; 149 | row = [self rowForFieldWithUserInfo:dic]; 150 | __weak typeof(self) weakSelf = self; 151 | row.didSelectCellBlock = ^(NSIndexPath *indexPath, id value, id cell){ 152 | /// 前往二级页面 153 | GenderPickerViewController *vc = [[GenderPickerViewController alloc] init]; 154 | vc.pickBlock = ^(BOOL isMale){ 155 | [weakSelf.navigationController popToViewController:weakSelf animated:YES]; 156 | 157 | value[kRightKey] = isMale ? @"Male" : @"Female"; 158 | [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle]; 159 | }; 160 | [weakSelf.navigationController pushViewController:vc animated:YES]; 161 | }; 162 | 163 | return row; 164 | } 165 | 166 | - (GSRow *)rowForStepper { 167 | GSRow *row = nil; 168 | 169 | row = [[GSRow alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"Stepper"]; 170 | row.cellClass = [StepperCell class]; 171 | row.rowHeight = 44.f; 172 | row.value = @{kLeftKey:@"Age"}.mutableCopy; 173 | row.rowConfigBlock = ^(StepperCell *cell, id value, NSIndexPath *indexPath){ 174 | cell.textLabel.text = value[kLeftKey]; 175 | [cell updateValue:[value[kRightKey] doubleValue]]; 176 | 177 | cell.stepperBlock = ^(double newValue){ 178 | value[kRightKey] = @(newValue); 179 | }; 180 | }; 181 | 182 | return row; 183 | } 184 | 185 | - (GSRow *)rowForBirthDay { 186 | GSRow *row = nil; 187 | 188 | NSDictionary *dic = @{kLeftKey:@"Date of Birth"}; 189 | row = [self rowForFieldWithUserInfo:dic]; 190 | 191 | /// 加入黑名单 192 | row.disableValidateBlock = ^NSDictionary *(id value, BOOL didClicked){ 193 | NSString *msg = @"此行已禁用,暂不支持"; 194 | if (didClicked) [self alertMsg:msg]; 195 | return rowError(msg); 196 | }; 197 | 198 | return row; 199 | } 200 | 201 | - (GSRow *)rowForFieldWithUserInfo:(NSDictionary *)userInfo { 202 | GSRow *row = nil; 203 | row = [[GSRow alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"lableFiled"]; 204 | row.rowHeight = 44.f; 205 | row.cellClass = [GSLabelFieldCell class]; 206 | 207 | row.value = userInfo.mutableCopy; 208 | row.rowConfigBlock = ^(GSLabelFieldCell *cell, id value, NSIndexPath *indexPath){ 209 | cell.leftlabel.text = value[kLeftKey]; 210 | cell.rightField.text = value[kRightKey]; 211 | cell.rightField.enabled = ![value[kDisableKey] boolValue]; 212 | cell.rightField.placeholder = value[kLeftKey]; 213 | 214 | BOOL hasArrow = [value[kFlagKey] boolValue]; 215 | cell.accessoryType = hasArrow ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; 216 | 217 | cell.textChangeBlock = ^(NSString *text){ 218 | value[kRightKey] = text; 219 | }; 220 | }; 221 | 222 | row.httpParamConfigBlock = ^(id value) { 223 | NSMutableDictionary *ret = [NSMutableDictionary dictionaryWithCapacity:1]; 224 | ret[value[kLeftKey]] = value[kRightKey]; 225 | return ret; 226 | }; 227 | 228 | return row; 229 | } 230 | 231 | - (void)alertMsg:(NSString *)msg title:(NSString *)title { 232 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:title 233 | message:msg 234 | preferredStyle:UIAlertControllerStyleAlert]; 235 | UIAlertAction *done = [UIAlertAction actionWithTitle:@"Done" style:UIAlertActionStyleDefault handler:nil]; 236 | [alert addAction:done]; 237 | [self presentViewController:alert animated:YES completion:nil]; 238 | } 239 | 240 | /// 弹窗 241 | - (void)alertMsg:(NSString *)msg { 242 | [self alertMsg:nil title:msg]; 243 | } 244 | 245 | @end 246 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/StepperCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface StepperCell : UITableViewCell 4 | 5 | @property (nonatomic, copy) void(^stepperBlock)(double newValue); 6 | 7 | - (void)updateValue:(double)value; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/StepperCell.m: -------------------------------------------------------------------------------- 1 | #import "StepperCell.h" 2 | 3 | @interface StepperCell () 4 | 5 | @property (nonatomic, strong) UIStepper *stepper; 6 | 7 | @end 8 | 9 | @implementation StepperCell 10 | #pragma mark - lifeCycle 11 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 12 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 13 | if (self) { 14 | [self.contentView addSubview:self.stepper]; 15 | 16 | self.selectionStyle = UITableViewCellSelectionStyleNone; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | - (void)layoutSubviews { 23 | [super layoutSubviews]; 24 | 25 | CGFloat centerX = self.contentView.frame.size.width - (self.stepper.bounds.size.width * 0.5 + 15); 26 | self.stepper.center = CGPointMake(centerX, self.contentView.frame.size.height * 0.5); 27 | 28 | self.detailTextLabel.frame = CGRectMake(0, 0, CGRectGetMinX(self.stepper.frame) - 15, self.contentView.frame.size.height); 29 | } 30 | 31 | - (void)updateValue:(double)value { 32 | self.detailTextLabel.text = [NSString stringWithFormat:@"%.0f", value]; 33 | _stepper.value = value; 34 | } 35 | 36 | #pragma mark - event reponse 37 | - (void)step:(UIStepper *)stepper { 38 | self.detailTextLabel.text = [NSString stringWithFormat:@"%.0f", stepper.value]; 39 | 40 | !self.stepperBlock ?: self.stepperBlock(stepper.value); 41 | } 42 | 43 | #pragma mark - setter/getter 44 | - (UIStepper *)stepper { 45 | if (_stepper) return _stepper; 46 | 47 | _stepper = [[UIStepper alloc] init]; 48 | _stepper.minimumValue = 0; 49 | _stepper.maximumValue = 100; 50 | _stepper.stepValue = 1; 51 | [_stepper addTarget:self action:@selector(step:) forControlEvents:UIControlEventValueChanged]; 52 | 53 | return _stepper; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/ViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ViewController : UIViewController 4 | 5 | @end 6 | 7 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/ViewController.m: -------------------------------------------------------------------------------- 1 | #import "ViewController.h" 2 | 3 | #import "RegisterViewController.h" 4 | 5 | @interface ViewController () 6 | 7 | @end 8 | 9 | @implementation ViewController 10 | 11 | - (void)viewDidLoad { 12 | [super viewDidLoad]; 13 | 14 | 15 | self.navigationController.navigationBar.translucent = NO; 16 | self.edgesForExtendedLayout = UIRectEdgeNone; 17 | self.navigationItem.title = @"GSForm"; 18 | 19 | UILabel *label = [[UILabel alloc] initWithFrame:self.view.bounds]; 20 | label.text = @"Tap here to push"; 21 | label.textAlignment = NSTextAlignmentCenter; 22 | label.textColor = [UIColor grayColor]; 23 | [self.view addSubview:label]; 24 | } 25 | 26 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 27 | RegisterViewController *vc = [[RegisterViewController alloc] init]; 28 | 29 | [self.navigationController pushViewController:vc animated:YES]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /GSFormDev/GSFormDev/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // GSFormDev 4 | // 5 | // Created by Brook on 2017/5/5. 6 | // Copyright © 2017年 Brook. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GSForm 2 | simple but powerful lib for Form 3 | 4 | 5 | ### 1.主要特点 6 | **轻量级,只有4个类,1个控制器```Controller```,3个视图模型```ViewModel```** 7 | 支持** iOS8 及以上 ** 8 | 9 | [GitHub 和 Demo 下载](https://github.com/beforeold/GSForm) 10 | 11 | 1. 支持完全自定义单元格```cell```类型 12 | - 支持自动布局```Autolayout```和固定行高 13 | - 表单每行```row```数据和事件整合为一个```model```,基本只需管理```row``` 14 | - 积木式组合 ```row```,支持 ```section``` 和 ```row``` 的隐藏,易于维护 15 | - 支持传入外部数据 16 | - 支持快速提取数据 17 | - 支持参数的最终合法性校验 18 | - 支持数据模型的类型完全自由自定义,可拆可合 19 | - 支持设置```row```的白名单和黑名单及权限管理 20 | 21 | ### 2.背景 22 | 23 | 通常,将一个页面需要编辑/录入多项信息的页面称为“表单页面”,以下称**表单**,以某注册页面为例: 24 | 25 | ![某注册页面](http://upload-images.jianshu.io/upload_images/73339-4224623071ef5884.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 26 | 在移动端进行表单的录入设计本身因为录入效率低,是尽量避免的,但对于特定的业务场景还是有存在的情况。通常基于 UITableView 进行开发,内容多有文本输入、日期(或者其他PickerView)、各类自定义的单元格```cell```(比如包含 UISwitch、UIStepper等)、以及一些需要前往二级页面获取信息后回调等元素。 27 | 28 | 表单的麻烦在于行与行之间数据往往没有特定的规律,上图中第二组数据中,姓名、性别、出生日期以及年龄,4个不同的 cell 则是 4个完全不同的交互方式来录入数据,依照传统的 UITableView 的代理模式来处理,有几个弊端: 29 | - 在实现数据源方法 ```tableView:cellForRowAtIndexPath:```难免要对每一个 indexPath 进行 switch-case 处理, 30 | - 糟糕的是对于每一行的点击事件,```tableView:didSelectRowAtIndexPath:````方法,也要进行 switch-case 判断 31 | - 因为 cell 的重用关系,每一行数据的取值也将严重依赖具体的 indexPath,数据的获取变得困难,同样地,编辑变化后的信息也需要存到到数据模型中,对于跳转二级页面回调的数据需要更新数据后要反过来刷新对应的```cell```。 32 | - 根据不同的入口,有一些 row 可能不存在或者需要临时插入 cell,这使得写死 indexPath 的 switch-case 很不可靠 33 | - 即便是静态页面的 cell,写死了 indexPath 进行 switch-case 在未来的需求调整时(比如调整了 row 的位置,新增/减少了某些 row),变得难以维护。 34 | 35 | ### 3.解决方案 36 | - 回顾上面的弊端,很大的一个弊病在于严重的依赖了 row 的位置 indexPath 来获取数据、绘制 cell、处理 cell 的事件以及回调刷新 row,借助 MVVM 的思路,将每一行的视图类型、视图刷新以及事件处理由每一行各自处理,用 GSRow 对象进行管理。 37 | - 单元格的构造,基于运行时和block,通过运行时构建cell,利用 row 对象的 cellClass/nibName 属性分别从代码或者 xib 加载可重用的 cell 视图备用 38 | - 调用 GSRow 的 configBlock 进行cell 内容的刷新和配置(包括了 cell内部的block回调事件) 39 | 40 | ```Objective-C 41 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 42 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 43 | 44 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.reuseIdentifier]; 45 | if (!cell) { 46 | if (row.cellClass) { 47 | /// 运行时加载 48 | cell = [[row.cellClass alloc] initWithStyle:row.style reuseIdentifier:row.reuseIdentifier]; 49 | } else { 50 | /// xib 加载 51 | cell = [[[NSBundle mainBundle] loadNibNamed:row.nibName owner:nil options:nil] lastObject]; 52 | } 53 | /// 额外的视图初始化 54 | !row.cellExtraInitBlock ?: row.cellExtraInitBlock(cell, row.value, indexPath); 55 | } 56 | 57 | NSAssert(!(row.rowConfigBlockWithCompletion && row.rowConfigBlock), @"row config block 二选一"); 58 | 59 | GSRowConfigCompletion completion = nil; 60 | if (row.rowConfigBlock) { 61 | /// cell 的配置方式一:直接配置 62 | row.rowConfigBlock(cell, row.value, indexPath); 63 | 64 | } else if (row.rowConfigBlockWithCompletion) { 65 | /// cell 的配置方式二:直接配置并返回最终配置 block 在返回cell前调用(可用作权限管理) 66 | completion = row.rowConfigBlockWithCompletion(cell, row.value, indexPath); 67 | } 68 | 69 | [self handleEnableForCell:cell gsRow:row atIndexPath:indexPath]; 70 | 71 | /// 在返回 cell 前做最终配置(可做权限控制) 72 | !completion ?: completion(); 73 | 74 | return cell; 75 | } 76 | ``` 77 | - 一个分组可以包含多个 GSRow 对象,在表单中对分组的头尾部视图并没有高度定制和复杂的事件回调,因此暂不做高度封装,主要提供作为 Row 的容器以及整体隐藏使用,即GSSection。 78 | 79 | ```Objective-C 80 | @interface GSSection : NSObject 81 | 82 | @property (nonatomic, strong, readonly) NSMutableArray *rowArray; 83 | @property (nonatomic, assign, readonly) NSUInteger count; 84 | @property (nonatomic, assign) CGFloat headerHeight; 85 | @property (nonatomic, assign) CGFloat footerHeight; 86 | @property (nonatomic, assign, getter=isHidden) BOOL hidden; 87 | 88 | `- (void)addRow:(GSRow *)row; 89 | `- (void)addRowArray:(NSArray *)rowArray; 90 | 91 | @end 92 | ``` 93 | - 同理,多个 GSSetion 对象在一个容器内进行管理会更便利,设置 GSForm 作为整个表单的容器,从而数据结构为GSForm 包含多个 GSSection,而 GSSection 包含多个 GSRow,这样与 UITableView 的数据源和代理结构保持一致。 94 | 95 | ```Objective-C 96 | @interface GSForm : NSObject 97 | 98 | @property (nonatomic, strong, readonly) NSMutableArray *sectionArray; 99 | @property (nonatomic, assign, readonly) NSUInteger count; 100 | 101 | @property (nonatomic, assign) CGFloat rowHeight; 102 | 103 | - (void)addSection:(GSSection *)section; 104 | - (void)removeSection:(GSSection *)section; 105 | 106 | - (void)reformRespRet:(id)resp; 107 | - (id)fetchHttpParams; 108 | 109 | - (NSDictionary *)validateRows; 110 | 111 | /// 配置全局禁用点击事件的block 112 | @property (nonatomic, copy) id(^disableBlock)(GSForm *); 113 | 114 | /// 根据 indexPath 返回 row 115 | - (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath; 116 | /// 根据 row 返回 indexPath 117 | - (NSIndexPath *)indexPathOfGSRow:(GSRow *)row; 118 | 119 | @end 120 | ``` 121 | 为了承载和实现 UITableView 的协议,将 UITabeView 作为控制器的子视图,设为 GSFormVC,GSFormVC 同时是 UITableView 的数据源dataSource 和代理 delegate,负责将 UITableView 的重要协议方法分发给 GSRow 和 GSSection,以及黑白名单控制,如此,具体的业务场景下,通过继承 GSFormVC 配置 GSForm 的结构,即可实现主体功能,对于分组section的头尾视图等可以通过在具体业务子类中实现 UITableView 的方式来实现即可。 122 | 123 | ### 4.具体功能点的实现 124 | #### 4.1 支持完全自定义单元格 cell 125 | 当 UITableView 的 tableView:cellForRowAtIndexPath:方法调用时,第一步时通过 row 的 reuserIdentifer 获取可重用的cell,当需要创建cell 时通过 GSRow 配置的 cellClass 属性或者 nibName 属性分别通过运行时或者 xib 创建新的cell 实例,从而隔离对 cell类型的直接依赖。 126 | 其中 GSRow 的构造方法 127 | 128 | ```Objective-C 129 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 130 | reuseIdentifier:(NSString *)reuseIdentifier; 131 | 132 | ``` 133 | 接着配置 cell 的具体类型,cellClass 或者 nibName 属性 134 | ```Objective-C 135 | @property (nonatomic, strong) Class cellClass; 136 | @property (nonatomic, strong) NSString *nibName; 137 | 138 | ``` 139 | 140 | 为了在 cell 初始化后可以进行额外的子视图构造或者样式配置,设置 GSRow 的 cellExtraInitBlock,将在 首次构造 cell 时进行额外调用,属性的声明: 141 | 142 | ```Objective-C 143 | @property (nonatomic, copy) void(^cellExtraInitBlock)(id cell, id value, NSIndexPath *indexPath); 144 | // if(!cell) { extraInitBlock }; 145 | 146 | ``` 147 | 148 | 下面是构造 cell 的处理 149 | ```Objective-C 150 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 151 | 152 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.reuseIdentifier]; 153 | if (!cell) { 154 | if (row.cellClass) { 155 | cell = [[row.cellClass alloc] initWithStyle:row.style reuseIdentifier:row.reuseIdentifier]; 156 | } else { 157 | cell = [[[NSBundle mainBundle] loadNibNamed:row.nibName owner:nil options:nil] lastObject]; 158 | } 159 | 160 | !row.cellExtraInitBlock ?: row.cellExtraInitBlock(cell, row.value, indexPath); 161 | } 162 | 163 | ``` 164 | 获取到构造的可用的cell 后需要利用数据模型对 cell 的内容进行填入处理,这个操作通过配置```rowConfigBlock``` 或者 ```rowConfigBlockWithCompletion``` 属性完成,这两个属性只会调用其中一个,后者的区别时会在配置完成后返回一个 block 变量用于进行最终配置,属性的声明如下: 165 | 166 | ```Objective-C 167 | @property (nonatomic, copy) void(^rowConfigBlock)(id cell, id value, NSIndexPath *indexPath); 168 | // config at cellForRowAtIndexPath: 169 | @property (nonatomic, copy) GSRowConfigCompletion(^rowConfigBlockWithCompletion)(id cell, id value, NSIndexPath *indexPath); 170 | // row config at cellForRow with extra final config 171 | ``` 172 | 173 | #### 4.2 支持自动布局```AutoLayout```和固定行高 174 | 自 iOS8 后 UITableView 支持高度自适应,通过在 GSFormVC 内对 TableView 进行自动布局的设置后,再在各个 Cell 实现各自的布局方案,表单的布局思路可以兼容固定行高和自动布局,TableView 的配置: 175 | ``` 176 | - (UITableView *)tableView { 177 | if (!_tableView) { 178 | _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; 179 | _tableView.delegate = self; 180 | _tableView.dataSource = self; 181 | _tableView.backgroundColor = [UIColor groupTableViewBackgroundColor]; 182 | _tableView.tableFooterView = [[UIView alloc] init]; 183 | _tableView.rowHeight = UITableViewAutomaticDimension; 184 | _tableView.estimatedRowHeight = 88.f; 185 | } 186 | 187 | return _tableView; 188 | } 189 | ``` 190 | 对应地,GSRow 的 rowHeight 属性可以实现 cell高度的固定,如果不传值则默认为自动布局,属性的声明: 191 | 192 | ```Objective-C 193 | @property (nonatomic, assign) CGFloat rowHeight; 194 | ``` 195 | 进而在 TableView 的代理中实现 cell 的高度布局,如下: 196 | ``` 197 | - (CGFloat)tableView:(UITableView *)tableView 198 | heightForRowAtIndexPath:(NSIndexPath *)indexPath { 199 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 200 | 201 | return row.rowHeight == 0 ? UITableViewAutomaticDimension : row.rowHeight; 202 | } 203 | ``` 204 | #### 4.3 表单每行row数据和事件整合为一个model,基本只需管理row 205 | 为了方便行数据的存储,设置了专门用于存值的属性,根据实际的需要进行赋值和取值即可,声明如下: 206 | 207 | ```Objective-C 208 | @property (nonatomic, strong) id value; 209 | ``` 210 | 在实际的应用中,value 使用可变字典的场景居多,如果内部有特定的自定义类对象,可以用一个key值保存在可变字典value中,方便存取,value 作为可变字典使用时有极大的自由便利性,可以在其中保存有规律的信息,比如表单cell 左侧的 title,右侧的内容等等,因为 block 可以时分便利地捕获上下对象,而且 GSForm 的设计实现时一个 GSRow 的几乎所有信息都在一个代码块内实现,从而实现上下文的共享,在上一个block存值时的key,可以在下一个block方便地得知用于取值和设值,比如一个 GSRow 的配置: 211 | 212 | ```Objective-C 213 | - (GSRow *)rowForTrace { 214 | GSRow *row = nil; 215 | 216 | GSTTraceListRespRet *model = [[GSTTraceListRespRet alloc] init]; 217 | row = [[GSRow alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GSLabelFieldCell"]; 218 | row.cellClass = [GSLabelFieldCell class]; 219 | row.rowHeight = 44; 220 | row.value = @{kCellLeftTitle:@"跟踪方案"}.mutableCopy; 221 | row.value[kCellModelKey] = model; 222 | row.rowConfigBlock = ^(GSLabelFieldCell *cell, id value, NSIndexPath *indexPath) { 223 | cell.leftlabel.text = value[kCellLeftTitle]; 224 | cell.rightField.text = model.name; 225 | cell.rightField.enabled = NO; 226 | cell.rightField.placeholder = @"请选择运输跟踪方案"; 227 | cell.accessoryView = form_makeArrow(); 228 | }; 229 | 230 | WEAK_SELF 231 | row.reformRespRetBlock = ^(GSTGoodsOriginInfoRespRet *ret, id value) { 232 | model.trace_id = ret.trace_id; 233 | model.name = ret.trace_name; 234 | }; 235 | 236 | row.didSelectBlock = ^(NSIndexPath *indexPath, id value) { 237 | STRONG_SELF 238 | GSTChooseTraceVC *ctl = [[GSTChooseTraceVC alloc] init]; 239 | ctl.chooseBlock = ^(GSTTraceListRespRet *trace){ 240 | model.trace_id = trace.trace_id; 241 | model.name = trace.name; 242 | [strongSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; 243 | }; 244 | [strongSelf.navigationController pushViewController:ctl animated:YES]; 245 | }; 246 | return row; 247 | } 248 | ``` 249 | 对于需要在点击 row 时跳转二级页面的情况,通过配置 GSRow 的 ```didSelectBlock``` 来实现,声明及示例如下: 250 | ```Objective-C 251 | @property (nonatomic, copy) void(^didSelectCellBlock)(NSIndexPath *indexPath, id value, id cell); 252 | // didSelectRow with Cell 253 | 254 | row.didSelectBlock = ^(NSIndexPath *indexPath, id value) { 255 | STRONG_SELF 256 | GSTChooseTraceVC *ctl = [[GSTChooseTraceVC alloc] init]; 257 | ctl.chooseBlock = ^(GSTTraceListRespRet *trace){ 258 | model.trace_id = trace.trace_id; 259 | model.name = trace.name; 260 | [strongSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; 261 | }; 262 | [strongSelf.navigationController pushViewController:ctl animated:YES]; 263 | }; 264 | ``` 265 | 通过对该属性的配置,在 TableView 的代理方法 tableView:didSelectRowAtIndexPath: 来调用: 266 | 267 | ```Objective-C 268 | - (void)tableView:(UITableView *)tableView 269 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 270 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 271 | 272 | GSRow *row = [self.form rowAtIndexPath:indexPath]; 273 | !row.didSelectBlock ?: row.didSelectBlock(indexPath, row.value); 274 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; 275 | !row.didSelectCellBlock ?: row.didSelectCellBlock(indexPath, row.value, cell); 276 | } 277 | 278 | ``` 279 | 综上,通过多个属性的配合使用,基本达成了 cell 的构造、配置和 cell内部事件以及 cell 整体点击事件的整合。 280 | 281 | #### 4.4 积木式组合 row,支持 section 和 row 的隐藏,易于维护 282 | 基于每行数据及其事件整合在 GSRow 内,具备了独立性,通过根据需求整合到不同的 GSSection 后即可搭建成具体的业务页面,举例: 283 | 284 | ```Objective-C 285 | /// 构造页面的表单数据 286 | - (void)buildDataSource { 287 | [self.form addSection:[self sectionChooseProject]]; 288 | [self.form addSection:[self sectionTransportSettings]]; 289 | [self.form addSection:[self sectionUploadAddress]]; 290 | [self.form addSection:[self sectionDownloadAdress]]; 291 | [self.form addSection:[self sectionOtherInfo]]; 292 | } 293 | ``` 294 | 此外,GSSection/GSRow 都支持隐藏,根据不同的场景设置 GSSection/GSRow 的隐藏状态,可以动态设置表单。 295 | ```Objective-C 296 | @property (nonatomic, assign, getter=isHidden) BOOL hidden; 297 | ``` 298 | 隐藏属性将通过 UITableView 的数据源 dataSource 协议方法决定是否显示 section/row: 299 | 300 | ```Objective-C 301 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 302 | NSInteger count = 0; 303 | for (GSSection *section in self.form.sectionArray) { 304 | if(!section.isHidden) count++; 305 | } 306 | 307 | return count; 308 | } 309 | 310 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 311 | GSSection *fSection = self.form[section]; 312 | NSInteger count = 0; 313 | for (GSRow *row in fSection.rowArray) { 314 | if(!row.isHidden) count++; 315 | } 316 | 317 | return count; 318 | } 319 | 320 | ``` 321 | 也正是因为GSSection/GSRow 的隐藏特点,根据 indexPath 取值时不能单方面地根据索引从数组中取值,也应考虑到是否有隐藏的对象,为此在 GSForm 定义了两个工具方法,用于关联 indexPath 与 GSRow 对象,在必要时调用。 322 | 323 | ```Objective-C 324 | /// 根据 indexPath 返回 row 325 | - (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath; 326 | /// 根据 row 返回 indexPath 327 | - (NSIndexPath *)indexPathOfGSRow:(GSRow *)row; 328 | ``` 329 | 通过这些可组合性,可以便利地搭建页面,且易于增删或者调整顺序。 330 | 331 | #### 4.5 支持传入外部数据 332 | 有些编辑类型的表单,首次加载时通过其他渠道加载数据后先填入一部分值,为此,GSRow 设计了从外部取值的属性 reformRespRetBlock,而外部参数经由 GSForm 进行遍历调用。 333 | 334 | ```Objective-C 335 | ///GSForm 336 | /// 传入外部数据 337 | - (void)reformRespRet:(id)resp; 338 | - (void)reformRespRet:(id)resp { 339 | for (GSSection *section in self.sectionArray) { 340 | for (GSRow *row in section.rowArray) { 341 | !row.reformRespRetBlock ?: row.reformRespRetBlock(resp, row.value); 342 | } 343 | } 344 | } 345 | /// GSRow 从外部取值的block配置 346 | @property (nonatomic, copy) void(^reformRespRetBlock)(id ret, id value); 347 | // 外部传值处理 348 | ``` 349 | 如此,通过网络请求的数据返回后调用 GSForm 将数据分发到 GSRow 存入到各自的 value 后,刷新 TableView 即可实现外部数据的导入,比如网络请求后调用构建页面各个 GSRow 并 传入外部数据: 350 | 351 | ```Objective-C 352 | 353 | SomeHTTPModel *result; // 网络请求成功返回值 354 | self.result = result; 355 | [self buildForm]; 356 | [self.form reformRespRet:result]; 357 | [self.tableView reloadData]; 358 | ``` 359 | 360 | #### 4.6 支持快速提取数据 361 | 对应地,当数据录入完成后,点击提交时,需要获取各行数据进行网络请求,此时根据业务场景各自通过,通过每个 GSRow 配置各自的请求参数即可,声明配置请求参数的属性 httpParamConfigBlock,以从表单中提取一个字典参数为例: 362 | 声明: 363 | ```Objective-C 364 | @property (nonatomic, copy) id(^httpParamConfigBlock)(id value); 365 | // get param for http request 366 | ``` 367 | 从表单中获取请求参数: 368 | ```Objective-C 369 | /// 获取当前请求参数 370 | - (NSMutableDictionary *)fetchCurrentRequestInfo { 371 | NSMutableDictionary *dic = [NSMutableDictionary dictionary]; 372 | for (GSSection *secion in self.form.sectionArray) { 373 | if (secion.isHidden) continue; 374 | 375 | for (GSRow *row in secion.rowArray) { 376 | if (row.isHidden || !row.httpParamConfigBlock) continue; 377 | 378 | id http = row.httpParamConfigBlock(row.value); 379 | if ([http isKindOfClass:[NSDictionary class]]) { 380 | [dic addEntriesFromDictionary:http]; 381 | } else if ([http isKindOfClass:[NSArray class]]) { 382 | for (NSDictionary *subHttp in http) { 383 | [dic addEntriesFromDictionary:subHttp]; 384 | } 385 | } 386 | } 387 | } 388 | return dic; 389 | } 390 | 391 | ``` 392 | #### 4.7 支持参数的最终合法性校验 393 | 一般地,对用户输入的参数在提交前需要进行合法性校验,对于较长的表单而言通常是点击提交按钮时进行,对参数的最终合法性进行逐个校验,当参数不合法时进行提醒,将合法性校验的要求声明为 GSRow 的属性进行处理,如下: 394 | 395 | ```Objective-C 396 | /// check isValid 397 | @property (nonatomic, copy) NSDictionary *(^valueValidateBlock)(id value); 398 | ``` 399 | 返回值为字典,其中字典的内容并不严格限制,一个好的实践是:用一个key 标记校验是否通过,另外一个key标记校验失败的提醒,比如: 400 | 401 | ```Objective-C 402 | row.valueValidateBlock = ^id(id value) { 403 | // 校验失败,返回一个 key 为 @NO 的字典,并携带错误地址。 404 | if(![value[kCellModelKey] count]) return rowError(@"XX时间不可为空"); 405 | 406 | return rowOK(); // 返回一个 key 为 @YES 的字典 407 | }; 408 | ``` 409 | 如此,可由整个表单 GSForm发起整体校验,做遍历处理,举例如下: 410 | 411 | ```Objective-C 412 | /// GSForm 413 | - (NSDictionary *)validateRows; 414 | - (NSDictionary *)validateRows { 415 | for (GSSection *section in self.sectionArray) { 416 | for (GSRow *row in section.rowArray) { 417 | if (!row.isHidden && row.valueValidateBlock) { 418 | NSDictionary *dic = row.valueValidateBlock(row.value); 419 | NSNumber *ret = dic[kValidateRetKey]; 420 | NSAssert(ret, @"必须有结果参数"); 421 | if (!ret) continue; 422 | if (!ret.boolValue) return dic; 423 | } 424 | } 425 | } 426 | 427 | return rowOK(); 428 | } 429 | 430 | // 业务方的使用 431 | /// 检查参数合法性,如不合法冒泡提醒 432 | - (BOOL)validateParameters { 433 | NSDictionary *validate = [self.form validateRows]; 434 | if (![validate[kValidateRetKey] boolValue]) { 435 | NSString *msg = validate[kValidateMsgKey]; // 错误提示信息 436 | [GSProgressHUD showWithTitle:msg inView:self.view]; 437 | return NO; 438 | } 439 | return YES; 440 | } 441 | 442 | ``` 443 | 444 | #### 4.8 支持数据模型的类型完全自由自定义,可拆可合 445 | 某一行的业务数据可以独立存在 GSRow 的value中,也可以直接使用 控制器外部的属性/实例变量,根据实际的情况便利性决定; 446 | 同理,在配置请求参数时,也可以根据网络层设计的需要决定,如果是配置一个自定义Model,则事先在外部声明懒加载一个请求参数,在 httpConfigBlock 中对应属性进行设值,如果是配置一个 字典,则可以独立提供一个 字典又或者干脆对外部的一个可变字典设值。 447 | 448 | #### 4.9 支持设置row的白名单和黑名单及权限管理 449 | 在特定的场景下,只能编辑个别cell,这些可以编辑的cell应加入**白名单**;在另外一个特定的场景下,不能编辑个别cell,这些不能编辑的cell应加入**黑名单**,在白黑名单之上,可能还夹杂一些特定权限的控制,使得只有特定权限时才可以编辑。针对这类需求,通过在 cell 视图上层覆盖一个可操作性拦截按钮进行处理,通过配置 GSRow 的 enableValidateBlock 和 disableValidateBlock 属性进行实现。 450 | 451 | ```Objective-C 452 | /// GSForm 453 | /// 传入此值实现全局禁用,此时点击事件的 block 454 | @property (nonatomic, copy) id(^disableBlock)(GSForm *); 455 | 456 | 457 | /// GSRow 的黑名单 458 | @property (nonatomic, copy) NSDictionary *(^disableValidateBlock)(id value, BOOL didClick); 459 | /// GSRow的白名单 460 | @property (nonatomic, copy) NSDictionary *(^enableValidateBlock)(id value, BOOL didClick); 461 | ``` 462 | 463 | ### 延伸 464 | 经过在项目中的应用,这个框架基本成型,并具备相当高的定制能力和灵活性,在后续的功能开发上会进一步迭代。 465 | 以下是几个注意点: 466 | - 在一些 cell 不规则/规则的静态页面,也适合使用。 467 | - 此框架处处都是 block 的应用,因此应格外注意避免循环引用的发生,因为 控制器持有 GSForm 和 UITableView,所以在 GSRow 的 block 属性配置,以及内部 GSRow配置 cell 的 cellConfigBlock 内又有 cell.textChangeBlock 这类情况,需要进行双重的弱引用处理,比如: 468 | 469 | ```Objective-C 470 | WEAK_SELF 471 | row.rowConfigBlock = ^(GSTCodeScanCell *cell, id value, NSIndexPath *indexPath) { 472 | STRONG_SELF 473 | cell.textChangeBlock = ^(NSString *text){ 474 | value[kCellRightContent] = text; 475 | }; 476 | 477 | /// 因为 cell 的block 是 强引用,所以这类需要再次设置弱引用。 478 | __weak typeof(strongSelf) weakWeakSelf = strongSelf; 479 | cell.scanClickBlock = ^(){ 480 | GSQRCodeController *scanVC = [[GSQRCodeController alloc] init]; 481 | scanVC.returnScanBarCodeValue = ^(NSString *str) { 482 | value[kCellRightContent] = str; 483 | [weakWeakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; 484 | }; 485 | [weakWeakSelf.navigationController pushViewController:scanVC animated:YES]; 486 | }; 487 | 488 | cell.selectionStyle = UITableViewCellSelectionStyleNone; 489 | }; 490 | ``` 491 | - 此外也有许多其他方案可供学习: 492 | 493 | 494 | 1. 最常提及的 [XLForm@Github](https://github.com/xmartlabs/XLForm)。 495 | - [简书J_Knight](http://www.jianshu.com/u/3dd433cb3ea1)前不久的[基于MVVM,用于快速搭建设置页,个人信息页的框架]。(http://www.jianshu.com/p/1f89513f3fb1) 496 | - [@靛青K](http://weibo.com/DianQK?refer_flag=1001030101_&is_all=1) 的 [iOS 上基于 RxSwift 的动态表单填写](http://t.cn/RXDsB9Z)。 497 | 498 | ### 分享 499 | [GitHub 和 Demo 下载](https://github.com/beforeold/GSForm) 500 | 501 | [个人博客](http://www.jianshu.com/u/7fb183d40a56) 502 | 503 | 欢迎你加入我的圈子讨论。 504 | ![](http://upload-images.jianshu.io/upload_images/73339-bdd16427cf564214.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 505 | --------------------------------------------------------------------------------