MCCSframework 教程(四)表单

除了极少部分纯展现的 APP,大部分 APP 都需要通过表单向用户搜集数据。MCCSframework 的表单符合 “MCCS” 的理念。主控制器(C)将表单界面托管给子控制器(S),子控制器通过 Cell 展现表单控件。子控制器将表单控件和模型(M)进行绑定,完成对用户输入数据的搜集。

接下来演示一个 APP 表单的例子。在这个例子中包含了键盘输入、日期选择、拍照/图片上传、下拉列表、true/false 单选框等 APP 中常见的用户输入方式。最终效果如下:

MCCSframework 教程(四)表单_第1张图片

模型

首先需要一个模型,以存储表单中的数据。新建类 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 和一个子控制器。

实现 Cell

新建一个 UICollectionViewCell 子类 MeetingTextFieldCell,勾选 Also create XIB file。

这将创建一个 .h 文件、一个 .m 文件和一个 .xib 文件。打开 .xib 文件,在 IB 中分别拖入一个UIView、一个 UILabel、一个 UITextField。创建必要的约束,效果如下图所示:

MCCSframework 教程(四)表单_第2张图片

分别为三个控件创建 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”,相信经过前面的教程,不需要做太多的解释。核心的地方有两处:

  1. tfCellAtIndex:title:placeholder:content 方法

    这个方法被 cellForItemAtIndex 方法调用,用于创建和返回我们刚刚创建的专门用于单行文本输入的 cell MeetingTextFieldCell。之所以不直接将代码写在 cellForItemAtIndex 方法里,是因为便于复用,因为这个表单中需要进行单行输入的 地方有好几处。这个方法你需要仔细阅读一下。

  2. 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];

运行效果如下:

MCCSframework 教程(四)表单_第3张图片

添加其他 MeetingTextFieldCell

除了会议主题,还有会议地点,会议主持会用到单行文本输入框,因此我们可以在子控制器中,增加两行 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,现在的界面变成:

MCCSframework 教程(四)表单_第4张图片

实现下拉列表输入

表单中的“单位”一项允许用户从下拉列表进行选择。它的数据来自于一个字符串数组。首先我们在子控制器中增加一个数组属性。打开 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 上:

MCCSframework 教程(四)表单_第5张图片

所以 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 :

MCCSframework 教程(四)表单_第6张图片

我们可以通过 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,效果如下图所示:

MCCSframework 教程(四)表单_第7张图片

日期选择

首先导入框架提供的 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,大概是这个样子的:

MCCSframework 教程(四)表单_第8张图片

分别是 4 个 UI 元素:1 个 UIView、2 个UILable、一个 UIImageView。

显示 SimpleDatePicker 控件

这个比较简单,调用它的 show 方法即可:

-(void)didSelectItemAtIndex:(NSInteger)index{
    ... ...

    }else if([self isBeginPicker:index]){
        [self.beginPicker show];
    }else if([self isEndPicker:index]){
        [self.endPicker show];
    }
}

运行 APP,如下图所示:

MCCSframework 教程(四)表单_第9张图片

点击开始/结束时间:

MCCSframework 教程(四)表单_第10张图片

单选

新建一个 CollectionViewCell 子类 MeetingRadioCell,本例表单中单选所用到的 cell 如下图所示,相信你已经能够很熟练地创建出来:

MCCSframework 教程(四)表单_第11张图片

MeetingRadioCell

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 后的效果:

MCCSframework 教程(四)表单_第12张图片

多行文本输入

首先仍然是从创建 cell 开始。新建一个 CollectionViewCell 子类 MeetingRemarkCell。设计其 UI 如下:

MCCSframework 教程(四)表单_第13张图片

创建相应的连接。

在子控制器 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 运行后的效果:

MCCSframework 教程(四)表单_第14张图片

接下来是什么?

接下来就是文件上传了。iOS 的图片(拍照/相册)上传要稍微复杂一些,我们计划在下一篇教程中单独介绍。感谢阅读,下次教程中再见。

你可能感兴趣的:(iPhone开发,MCCS)