輕量級,只有4個類,1個控制器Controller
,3個視圖模型ViewModel
支持** iOS8 及以上 **git
GitHub 和 Demo 下載github
cell
類型Autolayout
和固定行高row
數據和事件整合爲一個model
,基本只需管理row
row
,支持 section
和 row
的隱藏,易於維護row
的白名單和黑名單及權限管理一般,將一個頁面須要編輯/錄入多項信息的頁面稱爲「表單頁面」,如下稱表單,以某註冊頁面爲例:數組
在移動端進行表單的錄入設計自己由於錄入效率低,是儘可能避免的,但對於特定的業務場景仍是有存在的狀況。一般基於 UITableView 進行開發,內容多有文本輸入、日期(或者其餘PickerView)、各種自定義的單元格cell
(好比包含 UISwitch、UIStepper等)、以及一些須要前往二級頁面獲取信息後回調等元素。bash
表單的麻煩在於行與行之間數據每每沒有特定的規律,上圖中第二組數據中,姓名、性別、出生日期以及年齡,4個不一樣的 cell 則是 4個徹底不一樣的交互方式來錄入數據,依照傳統的 UITableView 的代理模式來處理,有幾個弊端:網絡
tableView:cellForRowAtIndexPath:
不免要對每個 indexPath 進行 switch-case 處理,cell
。- (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;
}
複製代碼
@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
複製代碼
@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 的方式來實現便可。數據結構
當 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
複製代碼
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;
}
複製代碼
爲了方便行數據的存儲,設置了專門用於存值的屬性,根據實際的須要進行賦值和取值便可,聲明以下:
@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:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
[strongSelf.navigationController pushViewController:ctl animated:YES];
};
複製代碼
經過對該屬性的配置,在 TableView 的代理方法 tableView:didSelectRowAtIndexPath: 來調用:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
GSRow *row = [self.form rowAtIndexPath:indexPath];
!row.didSelectBlock ?: row.didSelectBlock(indexPath, row.value);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
!row.didSelectCellBlock ?: row.didSelectCellBlock(indexPath, row.value, cell);
}
複製代碼
綜上,經過多個屬性的配合使用,基本達成了 cell 的構造、配置和 cell內部事件以及 cell 總體點擊事件的整合。
基於每行數據及其事件整合在 GSRow 內,具有了獨立性,經過根據需求整合到不一樣的 GSSection 後便可搭建成具體的業務頁面,舉例:
/// 構造頁面的表單數據
- (void)buildDataSource {
[self.form addSection:[self sectionChooseProject]];
[self.form addSection:[self sectionTransportSettings]];
[self.form addSection:[self sectionUploadAddress]];
[self.form addSection:[self sectionDownloadAdress]];
[self.form addSection:[self sectionOtherInfo]];
}
複製代碼
此外,GSSection/GSRow 都支持隱藏,根據不一樣的場景設置 GSSection/GSRow 的隱藏狀態,能夠動態設置表單。
@property (nonatomic, assign, getter=isHidden) BOOL hidden;
複製代碼
隱藏屬性將經過 UITableView 的數據源 dataSource 協議方法決定是否顯示 section/row:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = 0;
for (GSSection *section in self.form.sectionArray) {
if(!section.isHidden) count++;
}
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
GSSection *fSection = self.form[section];
NSInteger count = 0;
for (GSRow *row in fSection.rowArray) {
if(!row.isHidden) count++;
}
return count;
}
複製代碼
也正是由於GSSection/GSRow 的隱藏特色,根據 indexPath 取值時不能單方面地根據索引從數組中取值,也應考慮到是否有隱藏的對象,爲此在 GSForm 定義了兩個工具方法,用於關聯 indexPath 與 GSRow 對象,在必要時調用。
/// 根據 indexPath 返回 row
- (GSRow *)rowAtIndexPath:(NSIndexPath *)indexPath;
/// 根據 row 返回 indexPath
- (NSIndexPath *)indexPathOfGSRow:(GSRow *)row;
複製代碼
經過這些可組合性,能夠便利地搭建頁面,且易於增刪或者調整順序。
有些編輯類型的表單,首次加載時經過其餘渠道加載數據後先填入一部分值,爲此,GSRow 設計了從外部取值的屬性 reformRespRetBlock,而外部參數經由 GSForm 進行遍歷調用。
///GSForm
/// 傳入外部數據
- (void)reformRespRet:(id)resp;
- (void)reformRespRet:(id)resp {
for (GSSection *section in self.sectionArray) {
for (GSRow *row in section.rowArray) {
!row.reformRespRetBlock ?: row.reformRespRetBlock(resp, row.value);
}
}
}
/// GSRow 從外部取值的block配置
@property (nonatomic, copy) void(^reformRespRetBlock)(id ret, id value);
// 外部傳值處理
複製代碼
如此,經過網絡請求的數據返回後調用 GSForm 將數據分發到 GSRow 存入到各自的 value 後,刷新 TableView 便可實現外部數據的導入,好比網絡請求後調用構建頁面各個 GSRow 並 傳入外部數據:
SomeHTTPModel *result; // 網絡請求成功返回值
self.result = result;
[self buildForm];
[self.form reformRespRet:result];
[self.tableView reloadData];
複製代碼
對應地,當數據錄入完成後,點擊提交時,須要獲取各行數據進行網絡請求,此時根據業務場景各自經過,經過每一個 GSRow 配置各自的請求參數便可,聲明配置請求參數的屬性 httpParamConfigBlock,以從表單中提取一個字典參數爲例: 聲明:
@property (nonatomic, copy) id(^httpParamConfigBlock)(id value);
// get param for http request
複製代碼
從表單中獲取請求參數:
/// 獲取當前請求參數
- (NSMutableDictionary *)fetchCurrentRequestInfo {
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
for (GSSection *secion in self.form.sectionArray) {
if (secion.isHidden) continue;
for (GSRow *row in secion.rowArray) {
if (row.isHidden || !row.httpParamConfigBlock) continue;
id http = row.httpParamConfigBlock(row.value);
if ([http isKindOfClass:[NSDictionary class]]) {
[dic addEntriesFromDictionary:http];
} else if ([http isKindOfClass:[NSArray class]]) {
for (NSDictionary *subHttp in http) {
[dic addEntriesFromDictionary:subHttp];
}
}
}
}
return dic;
}
複製代碼
通常地,對用戶輸入的參數在提交前須要進行合法性校驗,對於較長的表單而言一般是點擊提交按鈕時進行,對參數的最終合法性進行逐個校驗,當參數不合法時進行提醒,將合法性校驗的要求聲明爲 GSRow 的屬性進行處理,以下:
/// check isValid
@property (nonatomic, copy) NSDictionary *(^valueValidateBlock)(id value);
複製代碼
返回值爲字典,其中字典的內容並不嚴格限制,一個好的實踐是:用一個key 標記校驗是否經過,另一個key標記校驗失敗的提醒,好比:
row.valueValidateBlock = ^id(id value) {
// 校驗失敗,返回一個 key 爲 @NO 的字典,並攜帶錯誤地址。
if(![value[kCellModelKey] count]) return rowError(@"XX時間不可爲空");
return rowOK(); // 返回一個 key 爲 @YES 的字典
};
複製代碼
如此,可由整個表單 GSForm發起總體校驗,作遍歷處理,舉例以下:
/// GSForm
- (NSDictionary *)validateRows;
- (NSDictionary *)validateRows {
for (GSSection *section in self.sectionArray) {
for (GSRow *row in section.rowArray) {
if (!row.isHidden && row.valueValidateBlock) {
NSDictionary *dic = row.valueValidateBlock(row.value);
NSNumber *ret = dic[kValidateRetKey];
NSAssert(ret, @"必須有結果參數");
if (!ret) continue;
if (!ret.boolValue) return dic;
}
}
}
return rowOK();
}
// 業務方的使用
/// 檢查參數合法性,如不合法冒泡提醒
- (BOOL)validateParameters {
NSDictionary *validate = [self.form validateRows];
if (![validate[kValidateRetKey] boolValue]) {
NSString *msg = validate[kValidateMsgKey]; // 錯誤提示信息
[GSProgressHUD showWithTitle:msg inView:self.view];
return NO;
}
return YES;
}
複製代碼
某一行的業務數據能夠獨立存在 GSRow 的value中,也能夠直接使用 控制器外部的屬性/實例變量,根據實際的狀況便利性決定; 同理,在配置請求參數時,也能夠根據網絡層設計的須要決定,若是是配置一個自定義Model,則事先在外部聲明懶加載一個請求參數,在 httpConfigBlock 中對應屬性進行設值,若是是配置一個 字典,則能夠獨立提供一個 字典又或者乾脆對外部的一個可變字典設值。
在特定的場景下,只能編輯個別cell,這些能夠編輯的cell應加入白名單;在另一個特定的場景下,不能編輯個別cell,這些不能編輯的cell應加入黑名單,在白黑名單之上,可能還夾雜一些特定權限的控制,使得只有特定權限時才能夠編輯。針對這類需求,經過在 cell 視圖上層覆蓋一個可操做性攔截按鈕進行處理,經過配置 GSRow 的 enableValidateBlock 和 disableValidateBlock 屬性進行實現。
/// GSForm
/// 傳入此值實現全局禁用,此時點擊事件的 block
@property (nonatomic, copy) id(^disableBlock)(GSForm *);
/// GSRow 的黑名單
@property (nonatomic, copy) NSDictionary *(^disableValidateBlock)(id value, BOOL didClick);
/// GSRow的白名單
@property (nonatomic, copy) NSDictionary *(^enableValidateBlock)(id value, BOOL didClick);
複製代碼
通過在項目中的應用,這個框架基本成型,並具有至關高的定製能力和靈活性,在後續的功能開發上會進一步迭代。 如下是幾個注意點:
WEAK_SELF
row.rowConfigBlock = ^(GSTCodeScanCell *cell, id value, NSIndexPath *indexPath) {
STRONG_SELF
cell.textChangeBlock = ^(NSString *text){
value[kCellRightContent] = text;
};
/// 由於 cell 的block 是 強引用,因此這類須要再次設置弱引用。
__weak typeof(strongSelf) weakWeakSelf = strongSelf;
cell.scanClickBlock = ^(){
GSQRCodeController *scanVC = [[GSQRCodeController alloc] init];
scanVC.returnScanBarCodeValue = ^(NSString *str) {
value[kCellRightContent] = str;
[weakWeakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
[weakWeakSelf.navigationController pushViewController:scanVC animated:YES];
};
cell.selectionStyle = UITableViewCellSelectionStyleNone;
};
複製代碼