SkillAgentSearch skills...

GSForm

simple but powerful lib for Form

Install / Use

/learn @beforeold/GSForm
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

GSForm

simple but powerful lib for Form

1.主要特点

轻量级,只有4个类,1个控制器Controller,3个视图模型ViewModel 支持** iOS8 及以上 **

GitHub 和 Demo 下载

  1. 支持完全自定义单元格cell类型
  • 支持自动布局Autolayout和固定行高
  • 表单每行row数据和事件整合为一个model,基本只需管理row
  • 积木式组合 row,支持 sectionrow 的隐藏,易于维护
  • 支持传入外部数据
  • 支持快速提取数据
  • 支持参数的最终合法性校验
  • 支持数据模型的类型完全自由自定义,可拆可合
  • 支持设置row的白名单和黑名单及权限管理

2.背景

通常,将一个页面需要编辑/录入多项信息的页面称为“表单页面”,以下称表单,以某注册页面为例:

某注册页面 在移动端进行表单的录入设计本身因为录入效率低,是尽量避免的,但对于特定的业务场景还是有存在的情况。通常基于 UITableView 进行开发,内容多有文本输入、日期(或者其他PickerView)、各类自定义的单元格cell(比如包含 UISwitch、UIStepper等)、以及一些需要前往二级页面获取信息后回调等元素。

表单的麻烦在于行与行之间数据往往没有特定的规律,上图中第二组数据中,姓名、性别、出生日期以及年龄,4个不同的 cell 则是 4个完全不同的交互方式来录入数据,依照传统的 UITableView 的代理模式来处理,有几个弊端:

  • 在实现数据源方法 tableView:cellForRowAtIndexPath:难免要对每一个 indexPath 进行 switch-case 处理,
  • 糟糕的是对于每一行的点击事件,```tableView:didSelectRowAtIndexPath:````方法,也要进行 switch-case 判断
  • 因为 cell 的重用关系,每一行数据的取值也将严重依赖具体的 indexPath,数据的获取变得困难,同样地,编辑变化后的信息也需要存到到数据模型中,对于跳转二级页面回调的数据需要更新数据后要反过来刷新对应的cell
  • 根据不同的入口,有一些 row 可能不存在或者需要临时插入 cell,这使得写死 indexPath 的 switch-case 很不可靠
  • 即便是静态页面的 cell,写死了 indexPath 进行 switch-case 在未来的需求调整时(比如调整了 row 的位置,新增/减少了某些 row),变得难以维护。

3.解决方案

  • 回顾上面的弊端,很大的一个弊病在于严重的依赖了 row 的位置 indexPath 来获取数据、绘制 cell、处理 cell 的事件以及回调刷新 row,借助 MVVM 的思路,将每一行的视图类型、视图刷新以及事件处理由每一行各自处理,用 GSRow 对象进行管理。
  • 单元格的构造,基于运行时和block,通过运行时构建cell,利用 row 对象的 cellClass/nibName 属性分别从代码或者 xib 加载可重用的 cell 视图备用
  • 调用 GSRow 的 configBlock 进行cell 内容的刷新和配置(包括了 cell内部的block回调事件)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    GSRow *row = [self.form rowAtIndexPath:indexPath];
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.reuseIdentifier];
    if (!cell) {
        if (row.cellClass) {
            /// 运行时加载
            cell = [[row.cellClass alloc] initWithStyle:row.style reuseIdentifier:row.reuseIdentifier];
        } else {
          /// xib 加载
            cell = [[[NSBundle mainBundle] loadNibNamed:row.nibName owner:nil options:nil] lastObject];
        }
        /// 额外的视图初始化
        !row.cellExtraInitBlock ?: row.cellExtraInitBlock(cell, row.value, indexPath);
    }
    
    NSAssert(!(row.rowConfigBlockWithCompletion && row.rowConfigBlock), @"row config block 二选一");
    
    GSRowConfigCompletion completion = nil;
    if (row.rowConfigBlock) {
        /// cell 的配置方式一:直接配置
        row.rowConfigBlock(cell, row.value, indexPath);
        
    } else if (row.rowConfigBlockWithCompletion) {
        /// cell 的配置方式二:直接配置并返回最终配置 block 在返回cell前调用(可用作权限管理)
        completion = row.rowConfigBlockWithCompletion(cell, row.value, indexPath);
    }
    
    [self handleEnableForCell:cell gsRow:row atIndexPath:indexPath];
    
    /// 在返回 cell 前做最终配置(可做权限控制)
    !completion ?: completion();
    
    return cell;
}
  • 一个分组可以包含多个 GSRow 对象,在表单中对分组的头尾部视图并没有高度定制和复杂的事件回调,因此暂不做高度封装,主要提供作为 Row 的容器以及整体隐藏使用,即GSSection。
@interface GSSection : NSObject

@property (nonatomic, strong, readonly) NSMutableArray <GSRow *> *rowArray;
@property (nonatomic, assign, readonly) NSUInteger count;
@property (nonatomic, assign) CGFloat headerHeight;
@property (nonatomic, assign) CGFloat footerHeight;
@property (nonatomic, assign, getter=isHidden) BOOL hidden;

`- (void)addRow:(GSRow *)row;
`- (void)addRowArray:(NSArray <GSRow *> *)rowArray;

@end
  • 同理,多个 GSSetion 对象在一个容器内进行管理会更便利,设置 GSForm 作为整个表单的容器,从而数据结构为GSForm 包含多个 GSSection,而 GSSection 包含多个 GSRow,这样与 UITableView 的数据源和代理结构保持一致。
@interface GSForm : NSObject

@property (nonatomic, strong, readonly) NSMutableArray <GSSection *> *sectionArray;
@property (nonatomic, assign, readonly) NSUInteger count;

@property (nonatomic, assign) CGFloat rowHeight;

- (void)addSection:(GSSection *)section;
- (void)removeSection:(GSSection *)section;

- (void)reformRespRet:(id)resp;
- (id)fetchHttpParams;

- (NSDictionary *)validateRows;

/// 配置全局禁用点击事件的block
@property (nonatomic, copy) id(^disableBlock)(GSForm *);

/// 根据 indexPath 返回 row
- (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath;
/// 根据 row 返回 indexPath
- (NSIndexPath *)indexPathOfGSRow:(GSRow *)row;

@end

为了承载和实现 UITableView 的协议,将 UITabeView 作为控制器的子视图,设为 GSFormVC,GSFormVC 同时是 UITableView 的数据源dataSource 和代理 delegate,负责将 UITableView 的重要协议方法分发给 GSRow 和 GSSection,以及黑白名单控制,如此,具体的业务场景下,通过继承 GSFormVC 配置 GSForm 的结构,即可实现主体功能,对于分组section的头尾视图等可以通过在具体业务子类中实现 UITableView 的方式来实现即可。

4.具体功能点的实现

4.1 支持完全自定义单元格 cell

当 UITableView 的 tableView:cellForRowAtIndexPath:方法调用时,第一步时通过 row 的 reuserIdentifer 获取可重用的cell,当需要创建cell 时通过 GSRow 配置的 cellClass 属性或者 nibName 属性分别通过运行时或者 xib 创建新的cell 实例,从而隔离对 cell类型的直接依赖。 其中 GSRow 的构造方法

- (instancetype)initWithStyle:(UITableViewCellStyle)style
              reuseIdentifier:(NSString *)reuseIdentifier;

接着配置 cell 的具体类型,cellClass 或者 nibName 属性

@property (nonatomic, strong) Class cellClass;
@property (nonatomic, strong) NSString *nibName;

为了在 cell 初始化后可以进行额外的子视图构造或者样式配置,设置 GSRow 的 cellExtraInitBlock,将在 首次构造 cell 时进行额外调用,属性的声明:

@property (nonatomic, copy) void(^cellExtraInitBlock)(id cell, id value, NSIndexPath *indexPath); 
// if(!cell) { extraInitBlock };

下面是构造 cell 的处理

    GSRow *row = [self.form rowAtIndexPath:indexPath];
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.reuseIdentifier];
    if (!cell) {
        if (row.cellClass) {
            cell = [[row.cellClass alloc] initWithStyle:row.style reuseIdentifier:row.reuseIdentifier];
        } else {
            cell = [[[NSBundle mainBundle] loadNibNamed:row.nibName owner:nil options:nil] lastObject];
        }
        
        !row.cellExtraInitBlock ?: row.cellExtraInitBlock(cell, row.value, indexPath);
    }

获取到构造的可用的cell 后需要利用数据模型对 cell 的内容进行填入处理,这个操作通过配置rowConfigBlock 或者 rowConfigBlockWithCompletion 属性完成,这两个属性只会调用其中一个,后者的区别时会在配置完成后返回一个 block 变量用于进行最终配置,属性的声明如下:

@property (nonatomic, copy) void(^rowConfigBlock)(id cell, id value, NSIndexPath *indexPath); 
// config at cellForRowAtIndexPath:
@property (nonatomic, copy) GSRowConfigCompletion(^rowConfigBlockWithCompletion)(id cell, id value, NSIndexPath *indexPath); 
// row config at cellForRow with extra final config

4.2 支持自动布局AutoLayout和固定行高

自 iOS8 后 UITableView 支持高度自适应,通过在 GSFormVC 内对 TableView 进行自动布局的设置后,再在各个 Cell 实现各自的布局方案,表单的布局思路可以兼容固定行高和自动布局,TableView 的配置:

- (UITableView *)tableView {
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        _tableView.backgroundColor = [UIColor groupTableViewBackgroundColor];
        _tableView.tableFooterView = [[UIView alloc] init];
        _tableView.rowHeight = UITableViewAutomaticDimension;
        _tableView.estimatedRowHeight = 88.f;
    }
    
    return _tableView;
}

对应地,GSRow 的 rowHeight 属性可以实现 cell高度的固定,如果不传值则默认为自动布局,属性的声明:

@property (nonatomic, assign) CGFloat rowHeight;

进而在 TableView 的代理中实现 cell 的高度布局,如下:

- (CGFloat)tableView:(UITableView *)tableView
        heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    GSRow *row = [self.form rowAtIndexPath:indexPath];
    
    return row.rowHeight == 0 ? UITableViewAutomaticDimension : row.rowHeight;
}

4.3 表单每行row数据和事件整合为一个model,基本只需管理row

为了方便行数据的存储,设置了专门用于存值的属性,根据实际的需要进行赋值和取值即可,声明如下:

@property (nonatomic, strong) id value;

在实际的应用中,value 使用可变字典的场景居多,如果内部有特定的自定义类对象,可以用一个key值保存在可变字典value中,方便存取,value 作为可变字典使用时有极大的自由便利性,可以在其中保存有规律的信息,比如表单cell 左侧的 title,右侧的内容等等,因为 block 可以时分便利地捕获上下对象,而且 GSForm 的设计实现时一个 GSRow 的几乎所有信息都在一个代码块内实现,从而实现上下文的共享,在上一个block存值时的key,可以在下一个block方便地得知用于取值和设值,比如一个 GSRow 的配置:

- (GSRow *)rowForTrace {
      GSRow *row = nil;
    
    GSTTraceListRespRet *model = [[GSTTraceListRespRet alloc] init];
    row = [[GSRow alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GSLabelFieldCell"];
    row.cellClass = [GSLabelFieldCell class];
    row.rowHeight = 44;
    row.value = @{kCellLeftTitle:@"跟踪方案"}.mutableCopy;
    row.value[kCellModelKey] = model;
    row.rowConfigBlock = ^(GSLabelFieldCell *cell, id value, NSIndexPath *indexPath) {
        cell.leftlabel.text = value[kCellLeftTitle];
        cell.rightField.text = model.name;
        cell.rightField.enabled = NO;
        cell.rightField.placeholder = @"请选择运输跟踪方案";
        cell.accessoryView = form_makeArrow();
    };    
    
    WEAK_SELF
    row.reformRespRetBlock = ^(GSTGoodsOriginInfoRespRet *ret, id value) {
        model.trace_id = ret.trace_id;
        model.name = ret.trace_name;
    };
    
    row.didSelectBlock = ^(NSIndexPath *indexPath, id value) {
        STRONG_SELF
        GSTChooseTraceVC *ctl = [[GSTChooseTraceVC alloc] init];
        ctl.chooseBlock = ^(GSTTraceListRespRet *trace){
            model.trace_id = trace.trace_id;
            model.name = trace.name;
            [strongSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        };
        [strongSelf.navigationController pushViewController:ctl animated:YES];
    };
  return row;
}

对于需要在点击 row 时跳转二级页面的情况,通过配置 GSRow 的 didSelectBlock 来实现,声明及示例如下:

@property (nonatomic, copy) void(^didSelectCellBlock)(NSIndexPath *indexPath, id value, id cell); 
// didSelectRow with Cell

    row.didSelectBlock = ^(NSIndexPath *indexPath, id value) {
        STRONG_SELF
        GSTChooseTraceVC *ctl = [[GSTChooseTraceVC alloc] init];
        ctl.chooseBlock = ^(GSTTraceListRespRet *trace){
            model.trace_id = trace.trace_id;
            model.name = trace.name;
            [strongSelf.tableView reloadRowsAtIndexPaths
View on GitHub
GitHub Stars41
CategoryDevelopment
Updated2mo ago
Forks5

Languages

Objective-C

Security Score

75/100

Audited on Jan 12, 2026

No findings