除了极少部分纯展现的 APP,大部分 APP 都需要通过表单向用户搜集数据。MCCSframework 的表单符合 “MCCS” 的理念。主控制器(C)将表单界面托管给子控制器(S),子控制器通过 Cell 展现表单控件。子控制器将表单控件和模型(M)进行绑定,完成对用户输入数据的搜集。
接下来演示一个 APP 表单的例子。在这个例子中包含了键盘输入、日期选择、拍照/图片上传、下拉列表、true/false 单选框等 APP 中常见的用户输入方式。最终效果如下:
首先需要一个模型,以存储表单中的数据。新建类 NewMeetingParam,类的定义如下:
#import
#import "MeetingInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface NewMeetingParam : NSObject
@property (strong, nonatomic) NSString* id;
@property (strong, nonatomic) NSString* isPublic;// "1" 公开 "0" 不公开
@property (strong, nonatomic) NSString* status;
@property(assign, nonatomic) int effect;
@property (strong, nonatomic) NSString* address;
@property (strong, nonatomic) NSString* title;
@property (strong, nonatomic) NSString* host;
@property (strong, nonatomic) NSString* remark;
@property (strong, nonatomic) NSString* beginTime;
@property (strong, nonatomic) NSString* endTime;
@property (strong, nonatomic) NSDictionary* baseFileStorePlus;
@property (strong, nonatomic) NSString* company;
@end
NS_ASSUME_NONNULL_END
我们首先从单行文本的输入开始。这需要分别实现一个 Cell 和一个子控制器。
新建一个 UICollectionViewCell 子类 MeetingTextFieldCell,勾选 Also create XIB file。
这将创建一个 .h 文件、一个 .m 文件和一个 .xib 文件。打开 .xib 文件,在 IB 中分别拖入一个UIView、一个 UILabel、一个 UITextField。创建必要的约束,效果如下图所示:
分别为三个控件创建 IBOutlet:
#import
#import
NS_ASSUME_NONNULL_BEGIN
@interface MeetingTextFieldCell : NibCollectionViewCell
@property (weak, nonatomic) IBOutlet UILabel *lbTitle;
@property (weak, nonatomic) IBOutlet UIView *line;
@property (weak, nonatomic) IBOutlet UITextField *tfText;
@end
NS_ASSUME_NONNULL_END
新建类 MeetingAddSC。类的定义如下:
#import
#import
#import "NewMeetingParam.h"
NS_ASSUME_NONNULL_BEGIN
@interface MeetingAddSC : SubController
@property (strong, nonatomic) NewMeetingParam* meetingParam;
@end
NS_ASSUME_NONNULL_END
meetingParam 用于搜集用户输入,就不多解释了。companyArr 是一个字符串数组,用于提供给下拉列表提供列表数据。
然后是类的实现:
#import "MeetingAddSC.h"
#import
#import
#import "MeetingTextFieldCell.h"
#import
@interface MeetingAddSC()
@end
@implementation MeetingAddSC
- (NSInteger)numberOfItems{
return 1;
}
- (CGSize)sizeForItemAtIndex:(NSInteger)index{
return CGSizeMake(SCREEN_WIDTH-20 , 60);
}
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
return [self tfCellAtIndex:index title:@"标题" placeholder:@"请输入会议主题" content:_meetingParam.title ];
}
// MARK: - Private
-(MeetingTextFieldCell *)tfCellAtIndex:(NSInteger)index title:(NSString*)title placeholder:(NSString*)placeholder content:(NSString* _Nullable)content{
MeetingTextFieldCell* cell = [self.collectionContext dequeueReusableCellOfClass:MeetingTextFieldCell.class forSectionController:self atIndex:index];
cell.contentView.backgroundColor = hex_color(0x3c4150);
cell.line.backgroundColor = hex_color(0x4a4f5e);
cell.line.hidden = NO;
cell.lbTitle.text = title;
cell.tfText.placeholder = placeholder;
cell.tfText.enabled = YES;
// 修改 placeholder 颜色
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:cell.tfText.font}];
cell.tfText.attributedPlaceholder = attrString;
cell.tfText.text = content;
cell.tfText.delegate = self;
cell.tfText.tag = index;
return cell;
}
// MARK: - UITextFieldDelegate
-(void)textFieldDidEndEditing:(UITextField *)textField{
switch (textField.tag) {
case 0:{// 标题
_meetingParam.title = textField.text;
break;
}
default:
break;
}
}
@end
代码非常的“MCCS”,相信经过前面的教程,不需要做太多的解释。核心的地方有两处:
tfCellAtIndex:title:placeholder:content 方法
这个方法被 cellForItemAtIndex 方法调用,用于创建和返回我们刚刚创建的专门用于单行文本输入的 cell MeetingTextFieldCell。之所以不直接将代码写在 cellForItemAtIndex 方法里,是因为便于复用,因为这个表单中需要进行单行输入的 地方有好几处。这个方法你需要仔细阅读一下。
UITextFieldDelegate
这个子控制器实现了 UITextFieldDelegate 协议。因为我们需要利用这个协议在适当的时机将 TextField 中的文本保存到 meetingParam 中去。如果你仔细看过 tfCellAtIndex:title:placeholder:content 方法,其中有 2 句:
cell.tfText.delegate = self;
cell.tfText.tag = index;
这是将 TextField 的 delegate 设置为子控制器了。然后子控制器实现了 textFieldDidEndEditing 委托方法。在其中根据 TextField 的 tag 属性来判断是哪个 cell 的 TextField。
在你的控制器中,将子控制器添加到主控制器的代码如下:
self.meetingSC = [MeetingAddSC new];
self.meetingSC.meetingParam = [NewMeetingParam new];
[self addSC:self.meetingSC];
[self.adapter reloadDataWithCompletion:nil];
运行效果如下:
除了会议主题,还有会议地点,会议主持会用到单行文本输入框,因此我们可以在子控制器中,增加两行 MeetingTextFieldCell。
首先,修改 numberOfItems 方法为:
- (NSInteger)numberOfItems{
return 3;
}
然后是 cellForItemAtIndex 方法:
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
if(index == 0){
return [self tfCellAtIndex:index title:@"标题" placeholder:@"请输入会议主题" content:_meetingParam.title ];
}else if(index==1){
return [self tfCellAtIndex:index title:@"地点" placeholder:@"请输入会议地点" content:_meetingParam.address ];
}else{
return [self tfCellAtIndex:index title:@"主持" placeholder:@"请输入会议主持" content:_meetingParam.host ];
}
}
同样需要修改的还有 textFieldDidEndEditing 方法:
// MARK: - UITextFieldDelegate
-(void)textFieldDidEndEditing:(UITextField *)textField{
switch (textField.tag) {
case 0:{// 标题
_meetingParam.title = textField.text;
break;
}
case 1:{// 地点
_meetingParam.address = textField.text;
break;
}
default:{// 主持
_meetingParam.host = textField.text;
break;
}
}
}
再次运行 APP,现在的界面变成:
表单中的“单位”一项允许用户从下拉列表进行选择。它的数据来自于一个字符串数组。首先我们在子控制器中增加一个数组属性。打开 MeetingAddSC.h,增加一个属性:
@property (strong, nonatomic) NSArray* companyArr;
我们用 companyArr 数组保存用户能够进行选择的单位列表。在构建子控制器时,我们需要初始化这个数组:
... ...
self.meetingSC = [MeetingAddSC new];
self.meetingSC.companyArr = @[@"施工单位",@"监理单位"];
... ...
然后打开 MeetingAddSC.m,增加以下几个属性:
@property(assign, nonatomic) BOOL companyListExpanded;// 单位地址是否展开
@property(assign, nonatomic) NSInteger selCompnayIndex;// 选中的单位索引
增加 2 个私有方法:
-(NSInteger)countOfCompanyArr{
return (_companyArr ? _companyArr.count : 0);
}
-(BOOL)isCompanyList:(NSInteger)index{
return index>3 && index <= 3+self.countOfCompanyArr;
}
前者返回 companyArr 数组元素的个数,后者根据 cell 索引判断该 cell 是否是单位列表中的 cell。
很显然,单位列表只会显示在 index 为 4 和 5 的 cell 上:
所以 isCompanyList 方法使用的逻辑表达式就是:
index>3 && index <= 3+self.countOfCompanyArr
为了简单起见,无论列表是否展开(即 companyListExpanded 无论为 yes 或 no),列表 cell 都是显示的,但是,当 companyListExpanded 为 NO 时,我们会将 cell 的高度设置为 0,从而达到隐藏列表的效果。因此在 sizeForItemAtIndex 方法会改成:
- (CGSize)sizeForItemAtIndex:(NSInteger)index{
if([self isCompanyList:index]){
return CGSizeMake(SCREEN_WIDTH-20 , _companyListExpanded ? 40 : 0);
}else{
return CGSizeMake(SCREEN_WIDTH-20 , 60);
}
}
首先判定 cell 是否是列表中的 cell,如果是,再判定列表当前的隐藏状态,如果是隐藏状态,高度设置为 0,否者设置为 40。
相应地,numberOfItems 方法也需要改为:
- (NSInteger)numberOfItems{
NSInteger i = 4;
i = i + self.countOfCompanyArr;
return i;
}
然后是 cellForItemAtIndex 方法:
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
if(index == 0){
return [self tfCellAtIndex:index title:@"标题" placeholder:@"请输入会议主题" content:_meetingParam.title ];
}else if(index==1){
return [self tfCellAtIndex:index title:@"地点" placeholder:@"请输入会议地点" content:_meetingParam.address ];
}else if(index==2){
return [self tfCellAtIndex:index title:@"主持" placeholder:@"请输入会议主持" content:_meetingParam.host ];
}else if(index==3){
MeetingTextFieldCell* cell = [self tfCellAtIndex:index title:@"单位" placeholder:@"选择单位" content:_meetingParam.company];
cell.tfText.enabled = NO;
return cell;
}else{
if(_companyListExpanded){
return [self labelCellAtIndex:index text:_companyArr[index-4]];
}else{
return [self spacerAtIndex:index];
}
}
从 index == 3 开始,就是“单位”下拉列表的 cell 了。当 index == 3 时,我们仍然使用单行文本输入 cell 显示,但 TextField 的 enabled 属性设为 NO 了,因为我们不需要用户从键盘输入,而是通过下面两行 cell 中进行选择。
在后面的两个 cell 中,我们使用 labelCellAtIndex:text 方法返回了新的 CollectionViewCell 子类:
-(MeetingCompanyCell*)labelCellAtIndex:(NSInteger)index text:(NSString*)text{
MeetingCompanyCell* cell = [self.collectionContext dequeueReusableCellOfClass:MeetingCompanyCell.class forSectionController:self atIndex:index];
cell.lbTitle.text = text;
cell.contentView.backgroundColor= [UIColor clearColor];
cell.line.backgroundColor = hex_color(0x4a4f5e);
cell.lineLeading.constant = [self isCompanyListEnd:index] ? 10 : 70;
return cell;
}
MeetingCompanyCell 是一个新的 CollectionViewCell 子类。这个 cell 非常简单,上面只有一个 UILabel (用于显示文本)和一个 UIView(用于显示一条分隔线),你可以自己设计这个 cell :
我们可以通过 didSelectItemAtIndex 方法来显示/隐藏单位列表,并处理用户在下拉列表中的点击:
-(void)didSelectItemAtIndex:(NSInteger)index{
if(index==3){
self.companyListExpanded = !self.companyListExpanded;
[self.collectionContext reloadSectionController:self];
}else if([self isCompanyList:index]){
self.selCompnayIndex = index-4;
self.meetingParam.company = self.companyArr[index-4];
self.companyListExpanded = NO;
[self.collectionContext reloadSectionController:self];
}
}
当用户点击到单位 cell 时,我们修改 companyListExpanded 属性,然后刷新子孔子器。这样子控制器会根据修改后的 companyListExpanded 属性动态设置列表 cell 的高度( 0 或者 40),从而隐藏或显示下拉列表。
如果用户选择了下拉列表中的某个 cell,则子控制器会更新自己的状态以及模型,然后刷新子控制器,从而更新 UI。
运行 APP,效果如下图所示:
首先导入框架提供的 SimpleDatePicker 组件:
#import
在子控制器中声明两个属性,一个用于开始时间,一个用于结束时间:
@property (strong, nonatomic) SimpleDatePicker* beginPicker;
@property (strong, nonatomic) SimpleDatePicker* endPicker;
以及它们的懒加载:
// MARK: - Lazy load
-(SimpleDatePicker*)beginPicker{
if (!_beginPicker) {
_beginPicker = [SimpleDatePicker new];
_beginPicker.dateMode = UIDatePickerModeDateAndTime;
_beginPicker.datePicker.locale = [[NSLocale alloc]initWithLocaleIdentifier:@"zh_CN"];
@weakify(self)
_beginPicker.okBlock = ^(NSDate* date){
@strongify(self)
NSString *str = dateToString(date,@"yyyy-MM-dd HH:mm");
self.meetingParam.beginTime = str;
[self.collectionContext reloadSectionController:self];
};
}
return _beginPicker;
}
-(SimpleDatePicker*)endPicker{
if (!_endPicker) {
_endPicker = [SimpleDatePicker new];
_endPicker.dateMode = UIDatePickerModeDateAndTime;
_endPicker.datePicker.locale = [[NSLocale alloc]initWithLocaleIdentifier:@"zh_CN"];
@weakify(self)
_endPicker.okBlock = ^(NSDate* date){
@strongify(self)
NSString *str = dateToString(date,@"yyyy-MM-dd HH:mm");
self.meetingParam.endTime = str;
[self.collectionContext reloadSectionController:self];
};
}
return _endPicker;
}
注意,这两个日期选择控件的 okBlock 块中,我们修改了模型值,刷新了 UI。
子控制器中,增加两个实用工具方法:
-(BOOL)isBeginPicker:(NSInteger)index{
return index == 4+self.countOfCompanyArr;
}
-(BOOL)isEndPicker:(NSInteger)index{
return index == 5+self.countOfCompanyArr;
}
用这两个方法来根据 index 判断 cell 是开始日期选择控件还是结束日期选择控件。
先在原来 cell 数量的基础上 + 2 :
- (NSInteger)numberOfItems{
NSInteger i = 6;
i = i + self.countOfCompanyArr;
return i;
}
cellForIndex 方法修改:
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
... ...
}else if([self isCompanyList:index]){
if(_companyListExpanded){
return [self labelCellAtIndex:index text:_companyArr[index-4]];
}else{
return [self spacerAtIndex:index];
}
}else if([self isBeginPicker:index]){
return [self pickCellAtIndex:index title:@"开始时间" content:_meetingParam.beginTime?:@"请选择"];
}else if([self isEndPicker:index]){
return [self pickCellAtIndex:index title:@"结束时间" content:_meetingParam.endTime?:@"请选择"];
}else{
return [self spacerAtIndex:index];
}
}
其中调用到的 pickerCellAtIndex:title:content 方法定义如下:
-(MeetingPickerCell*)pickCellAtIndex:(NSInteger)index title:(NSString*)title content:(NSString*)content{
MeetingPickerCell* cell = [self.collectionContext dequeueReusableCellOfClass:MeetingPickerCell.class forSectionController:self atIndex:index];
cell.lbTitle.text = title;
cell.lbContent.text = content;
cell.line.backgroundColor = hex_color(0x4a4f5e);
cell.line.hidden = NO;
return cell;
}
MeetingPickerCell 是我们新增的 CollectionViewCell。打开 MeetingPickerCell.xib,大概是这个样子的:
分别是 4 个 UI 元素:1 个 UIView、2 个UILable、一个 UIImageView。
这个比较简单,调用它的 show 方法即可:
-(void)didSelectItemAtIndex:(NSInteger)index{
... ...
}else if([self isBeginPicker:index]){
[self.beginPicker show];
}else if([self isEndPicker:index]){
[self.endPicker show];
}
}
运行 APP,如下图所示:
点击开始/结束时间:
新建一个 CollectionViewCell 子类 MeetingRadioCell,本例表单中单选所用到的 cell 如下图所示,相信你已经能够很熟练地创建出来:
MeetingRadioCell 中需要编写点代码。在 MeetingRadioCell.h 中,声明 2 个属性:
@property (nonatomic,assign) BOOL selectedBool;
@property (strong, nonatomic) void(^onselect)(BOOL on);
前者用于保存用户选择( Yes/No)。后者提供一个块,允许当用户改变选择后通知子控制器执行一些操作,比如刷新 UI。
在 MeetingRadioCell.m 中:
@implementation MeetingRadioCell
@synthesize selectedBool=_selectedBool;
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
self.selectedBool =NO;
}
- (IBAction)onBtNo:(id)sender {
self.selectedBool=NO;
}
- (IBAction)onBtYes:(id)sender {
self.selectedBool = YES;
}
-(BOOL)selectedBool{
return _selectedBool;
}
-(void)setSelectedBool:(BOOL)on{
_selectedBool= on;
_btYes.selected = on;
_btNo.selected = !on;
if(_onselect)
_onselect(on);
}
@end
很简单,无论用户选择了哪个按钮,另外一颗按钮将会 deselect,同时调用 onselect 块。
在子控制器中,cell 数目再次加 1:
- (NSInteger)numberOfItems{
NSInteger i = 7;
i = i + self.countOfCompanyArr;
return i;
}
然后定义一个工具方法:
-(BOOL)isPublicCell:(NSInteger)index{
return index == 6+self.countOfCompanyArr;
}
在 cellForItemAtIndex 中增加这个 cell:
... ...
}else if([self isPublicCell:index]){
MeetingRadioCell* cell=[self radioCellAtIndex:index];
cell.selectedBool = @"1".equal(_meetingInfo.isPublic);
return cell;
}
... ...
这是 cell 的实例化方法:
-(MeetingRadioCell*)radioCellAtIndex:(NSInteger)index{
MeetingRadioCell* cell = [self.collectionContext dequeueReusableCellOfClass:MeetingRadioCell.class forSectionController:self atIndex:index];
cell.line.backgroundColor = hex_color(0x4a4f5e);
cell.line.hidden = NO;
@weakify(self)
cell.onselect = ^(BOOL on) {
@strongify(self)
self.meetingInfo.isPublic = on ? @"1":@"0";
[self.collectionContext reloadSectionController:self];
};
return cell;
}
这是运行 APP 后的效果:
首先仍然是从创建 cell 开始。新建一个 CollectionViewCell 子类 MeetingRemarkCell。设计其 UI 如下:
创建相应的连接。
在子控制器 MeetingAddSC 中,cell 的数量再次 +1:
- (NSInteger)numberOfItems{
NSInteger i = 8;
i = i + self.countOfCompanyArr;
return i;
}
定义一个工具方法:
-(BOOL)isRemarkCell:(NSInteger)index{
return index == 7+self.countOfCompanyArr;
}
在 cellForItemAtIndex 方法中:
}else if([self isRemarkCell:index]){
MeetingRemarkCell* cell = [self remarkCellAtIndex:index];
cell.tvContent.text= _meetingInfo.remark;
return cell;
}
cell 的实例化方法:
-(MeetingRemarkCell*)remarkCellAtIndex:(NSInteger)index{
MeetingRemarkCell* cell = [self.collectionContext dequeueReusableCellOfClass:MeetingRemarkCell.class forSectionController:self atIndex:index];
cell.line.backgroundColor = hex_color(0x4a4f5e);
cell.line.hidden = NO;
cell.tvContent.delegate = self;
cell.tvContent.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"请输入备注" attributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:cell.lbTitle.font}];;
return cell;
}
然后,让子控制器实现 UITextViewDelegate 协议:
// MARK: - UITextViewDelegate
-(void)textViewDidEndEditing:(UITextView *)textView{
_meetingInfo.remark= textView.text;
}
这样,就可以将用户输入保存到实体模型。
APP 运行后的效果:
接下来就是文件上传了。iOS 的图片(拍照/相册)上传要稍微复杂一些,我们计划在下一篇教程中单独介绍。感谢阅读,下次教程中再见。