写一个 iOS 复杂表单的正确姿势
前言
这几天项目的新需求中有个复杂的表单界面,在做的过程中发现要比想象中复杂很多,有好多问题需要处理。有很多东西值得写下来好好梳理下。
需求分析:

上图便是UI根据需求给的高保真, 我们先根据这张图片来描述一下具体需求,明确一下我们都需要干些什么。
创建网店这个界面是一个复杂的表单,有“网店名称”、“网店主标签”、“网店简介”、“网店地址”、“网店座机”、“email”、“网店LOGO”、“网店封面图”这些项。大部分都是输入框,但也有几项有所不同。“网店地址”项,当被点击后会弹出一个pickView
来选择“市&区”;“网店LOGO”和“网店封面图”是一样的,是选取图片的控件,要求既可以通过相册选取图片,也可以现场拍照选择。当被点击后,弹出一个ActionSheet
来是以“拍照”或以“相册”来选取图片。当选取成功后拍照的背景图片变为被选取的图片,并在右上角出现一个删除按钮,可以删除还原再次选取。
表单中除了“email”外所有的项目都是必填的,且“网店名称”、“网店主标签”、“网店简介”和“网店座机”分别有30、20、500、15字的长度限制。“email”虽然为选填,但若填写了则会进行邮箱格式校验。对字数长度的限制要在输入过程中进行监听,若输入时超过限制,则输入框出现红色边框并出现提示文字。等最后点击了“提交”按钮后要进行数据校验,所有该填但未填,所有格式不正确的项都会出现红框和提示文字,当所有数据都合法后才可以提交给服务器。
需求大体就是如此。
这个界面我们还是以tableView
来实现,由cell
视图来表示图中所需填写的项目。那我们得先分析下这个界面需要写哪几种样式的cell
。
该界面总共有4种样式的cell
。4种样式的cell
样式也有共同点,每个cell
左边部分均为表示该行所要填写的项目名称,右边部分则为填写或者选取的内容值,这些值的显示形式有所不同。 CreateShopTFCell
和CreateShopTVCell
其实非常类似,右边首先是一个灰色的背景视图,只不过在灰色背景之上的前者是textField
,而后者是textView
;CreateShopPickCell
右边则是两个灰色背景视图,点击之后便弹出一个pickView
供你选取“市&区”;CreateShopUploadPicCell
右边则是一个UIImageView
,无图片被选取时默认是一个相机的图片,当被点击后弹出ActionSheet
供你选择拍照还是从相册选取照片,选好照片后UIImageView
的图片被替换,并在右上角出现红色的删除按钮。
如下图所示:

正确地将视图和数据绑定:
我们假设已经写好了上面4种样式cell
的代码,现在我们在控制器里为其填充数据。
我们首先定义一个表示cell
数据的CreateShopModel
。该model
是为了给cell
填充数据,可以看到它里面的属性就是cell
上对应应该显示的数据项。
同时,我们在开头也定义了一个枚举CreateShopCellType
来代表4种不同样式的cell
,用于在tableView
返回cell
的代理方法里根据枚举值来返回相应样式的cell
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #import typedef enum : NSUInteger { CreateShopCellType_TF = 0, // textfield CreateShopCellType_TV, // textView CreateShopCellType_PICK, // picker CreateShopCellType_PIC, // upload picture } CreateShopCellType; @interface CreateShopModel : NSObject @property (nonatomic, copy)NSString *title; // 所要填写的项目名称 @property (nonatomic, copy)NSString *placeholder; @property (nonatomic, copy)NSString *key; // 表单对应的字段 @property (nonatomic, copy)NSString *errText; // 校验出错时的提示信息 @property (nonatomic, strong)UIImage *image; // 所选取的图片 @property (nonatomic, assign)CreateShopCellType cellType; // cell的类型 @property (nonatomic, assign)NSInteger maxInputLength; // 最大输入长度限制 @end |
我们在将tableView
创建并添加在控制器的view
上后便可以初始化数据源了。该界面tableView
的数据源是_tableViewData
数组,数据的每项元素是代表cell
显示数据的CreateShopModel
类型的model
。准确地来说,这些数据是表单未填写之前的死数据,所以需要我们手动地给装入数据源数组中。而在输入框输入或者选取而得的数据则需要我们在输入之后将其捕获存储下来,以等到提交时提交给服务器,这个也有需要注意的坑点,后面再说。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | - (void)intDataSource { _tableViewData = [NSMutableArray array]; CreateShopModel *nameModel = [[CreateShopModel alloc] init]; nameModel.title = @"网店名称"; nameModel.placeholder = @"请输入网店名称"; nameModel.key = @"groupName"; nameModel.cellType = CreateShopCellType_TF; nameModel.maxInputLength = 30; [_tableViewData addObject:nameModel]; CreateShopModel *mainTagModel = [[CreateShopModel alloc] init]; mainTagModel.title = @"网店主标签"; mainTagModel.placeholder = @"请输入网店主标签"; mainTagModel.key = @"tag"; mainTagModel.cellType = CreateShopCellType_TF; mainTagModel.maxInputLength = 20; [_tableViewData addObject:mainTagModel]; CreateShopModel *descModel = [[CreateShopModel alloc] init]; descModel.title = @"网店简介"; descModel.placeholder = @"请输入网店简介"; descModel.key = @"introduction"; descModel.cellType = CreateShopCellType_TV; descModel.maxInputLength = 500; [_tableViewData addObject:descModel]; CreateShopModel *addressModel = [[CreateShopModel alloc] init]; addressModel.title = @"网店地址"; addressModel.placeholder = @""; addressModel.key = @"regionId"; addressModel.cellType = CreateShopCellType_PICK; [_tableViewData addObject:addressModel]; CreateShopModel *doorIDModel = [[CreateShopModel alloc] init]; doorIDModel.title = @""; doorIDModel.placeholder = @"请输入详细门牌号"; doorIDModel.key = @"address"; doorIDModel.cellType = CreateShopCellType_TF; [_tableViewData addObject:doorIDModel]; CreateShopModel *phoneModel = [[CreateShopModel alloc] init]; phoneModel.title = @"网店座机"; phoneModel.placeholder = @"请输入网店座机"; phoneModel.key = @"telephone"; phoneModel.cellType = CreateShopCellType_TF; phoneModel.maxInputLength = 15; [_tableViewData addObject:phoneModel]; CreateShopModel *emailModel = [[CreateShopModel alloc] init]; emailModel.title = @"email"; emailModel.placeholder = @"请输入email(选填)"; emailModel.key = @"contactMail"; emailModel.cellType = CreateShopCellType_TF; [_tableViewData addObject:emailModel]; CreateShopModel *logoModel = [[CreateShopModel alloc] init]; logoModel.title = @"网店LOGO"; logoModel.placeholder = @""; logoModel.key = @"logo"; logoModel.urlKey = @"logoUrl"; logoModel.cellType = CreateShopCellType_PIC; [_tableViewData addObject:logoModel]; CreateShopModel *coverPicModel = [[CreateShopModel alloc] init]; coverPicModel.title = @"网店封面图"; coverPicModel.placeholder = @""; coverPicModel.key = @"cover"; coverPicModel.urlKey = @"coverUrl"; coverPicModel.cellType = CreateShopCellType_PIC; [_tableViewData addObject:coverPicModel]; if(_tableView){ [_tableView reloadData]; } } |
现在我们的数据源准备好了,但是tableView
还没做处理呢,要等tableView
也配套完成后再刷新tableView
就OK了。我们来看tableView
代理方法。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _tableViewData.count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CreateShopModel *createModel = _tableViewData[indexPath.row]; if(createModel.cellType == CreateShopCellType_TF){ return [CreateShopTFCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_TV){ return [CreateShopTVCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_PICK){ return [CreateShopPickCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_PIC){ return [CreateShopUploadPicCell cellHeight: createModel]; } return 50.f; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CreateShopModel *createModel = _tableViewData[indexPath.row]; if(createModel.cellType == CreateShopCellType_TF) { static NSString *tfCellId = @"tfCellId"; CreateShopTFCell *cell = [tableView dequeueReusableCellWithIdentifier:tfCellId]; if(cell==nil) { cell = [[CreateShopTFCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tfCellId]; cell.cellDelegate = self; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_TV) { static NSString *tvCellId = @"tvCellId"; CreateShopTVCell *cell = [tableView dequeueReusableCellWithIdentifier:tvCellId]; if(cell==nil) { cell = [[CreateShopTVCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tvCellId]; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_PICK) { static NSString *pickCellId = @"pickCellId"; CreateShopPickCell *cell = [tableView dequeueReusableCellWithIdentifier:pickCellId]; if(cell==nil) { cell = [[CreateShopPickCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:pickCellId]; } NSString *valueStr = [_shopFormModel valueForKey:createModel.key]; if(valueStr.length>0){ createModel.errText = @""; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_PIC) { static NSString *picCellId = @"picCellId"; CreateShopUploadPicCell *cell = [tableView dequeueReusableCellWithIdentifier:picCellId]; if(cell==nil) { cell = [[CreateShopUploadPicCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:picCellId]; } id value = [_shopFormModel valueForKey:createModel.key]; if([value isKindOfClass:[NSString class]]){ NSString *valueStr = (NSString *)value; if(valueStr.length>0){ createModel.errText = @""; } } else if([value isKindOfClass:[UIImage class]]){ UIImage *valueImg = (UIImage *)value; if(valueImg){ createModel.errText = @""; } } __weak CreateShopViewController *weakSelf = self; [cell refreshContent:createModel formModel:_shopFormModel editBlock:^(CreateShopModel *shop) { if (shop) { _shopFormModel.indexPath = indexPath; _shopFormModel.indexPathObj = shop; [weakSelf iconActionSheet]; } }]; return cell; } return nil; } |
首先比较简单的,在设置行高的代理方法里,根据该行数据所表示的cellType
类型来设置相应的行高。
然后在返回cell
的代理方法里,同样以cellType
来判断返回相应样式的cell
,并给该cell
赋相应的数据model
。但是我们注意到,给cell
赋值的方法,除了传入我们前面说定义的CreateShopModel
类型的createModel
外,还有个名叫_shopFormModel
参数被传入。_shopFormModel
是什么,它代表什么意思?
_shopFormModel
是CreateShopFormModel
类型的一个实例对象,它用来表示这个表单需要提交的数据,它里面的每个属性基本上对应着表单提交给服务器的字段。我们最后不是要将表单数据作为参数去请求提交的接口吗?表单数据从哪里来,就从_shopFormModel
中来。那_shopFormModel
中的数据从哪里来?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #import @interface CreateShopFormModel : NSObject @property (nonatomic, copy)NSString *groupId; @property (nonatomic, copy)NSString *groupName; @property (nonatomic, copy)NSString *tag; @property (nonatomic, copy)NSString *introduction; @property (nonatomic, copy)NSString *regionId; @property (nonatomic, copy)NSString *cityId; @property (nonatomic, copy)NSString *address; @property (nonatomic, copy)NSString *telephone; @property (nonatomic, copy)NSString *contactMail; @property (nonatomic, copy)NSString *coverUrl; @property (nonatomic, copy)NSString *logoUrl; @property (nonatomic, strong)UIImage *logo; @property (nonatomic, strong)UIImage *cover; @property (nonatomic, strong)NSIndexPath *indexPath; @property (nonatomic, strong)id indexPathObj; + (CreateShopFormModel *)formModelFromDict:(NSDictionary *)dict; -(BOOL)submitCheck:(NSArray*)dataArr; @end |
以CreateShopTFCell
为例,它所表示的字段的数据是我们在输入框输入的,也就是说数据来自textField
,_shopFormModel
对象在控制器被传入cell
的refreshContent:formModel:
方法,在该方法内部,将参数formModel
赋给成员变量_formModel
。需要格外注意的是,_shopFormModel
、formModel
和_ formModel
是同一个对象,指向的是同一块内存地址。方法传递对象参数时只是“引用拷贝”,拷贝了一份对象的引用。既然这样,我们可以预想到,我们在cell
内部,将textField
输入的值赋给_formModel
所指向的对象后,也即意味着控制器里的_shopFormModel
也有数据了,因为它们本来就是同一个对象嘛!
事实正是如此。
可以看到我们在给textField
添加的通知的回调方法textFiledEditChanged:
里,将textField
输入的值以KVC
的方式赋值给了_formModel
。此时_formModel
的某属性,即该cell
对应的表单的字段已经有了数据。同样的,在控制器中与_formModel
指向同一块内存地址的_shopFormModel
也有了数据。
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 51 52 | - (void)clearCellData { _titleLab.text = @""; _textField.text = @""; _textField.placeholder = @""; _checkTipLab.text = @""; } - (void)refreshContent:(CreateShopModel *)createModel formModel:(CreateShopFormModel *)formModel { [self clearCellData]; if(!createModel){ return; } _createModel = createModel; _formModel = formModel; _titleLab.text = createModel.title; _textField.placeholder = createModel.placeholder; _textField.text = [_formModel valueForKey:createModel.key]; // 将_formModel的值以KVC的方式赋给textField if(createModel.errText.length>0){ _bgView.layer.borderColor = HexColor(0xcc2929).CGColor; _checkTipLab.text = createModel.errText; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } } - (void)textFiledEditChanged:(NSNotification *)obj { UITextField *textField = (UITextField *)obj.object; NSString *toBeString = textField.text; [_formModel setValue:textField.text forKey:_createModel.key]; // 将textField中的值赋给_formModel if(_createModel.maxInputLength&&toBeString.length>0&&toBeString.length>_createModel.maxInputLength){ _bgView.layer.borderColor = HexColor(0xcc2929).CGColor; _checkTipLab.text = [NSString stringWithFormat:@"最多%d个字",(int)_createModel.maxInputLength]; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } if([_createModel.key isEqualToString:@"contactMail"]){ _createModel.errText = @""; }else{ NSString *valueStr = [_formModel valueForKey:_createModel.key]; if(valueStr.length>0){ _createModel.errText = @""; } } } |
我们看到在refreshContent:formModel:
方法中,cell
上的死数据是被CreateShopModel
的实例对象createModel
赋值的,而在其后我们又以KVC
的方式又将_shopFormModel
的某属性的值赋给了textField
。这是因为我们为了防止cell
在复用的过程中出现数据错乱的问题,而在给cell
赋值前先将每个视图上的数据都清空了(即clearCellData
方法),需要我们重新赋过。(不过,如果你没清空数据的情况下,不再次给textField
赋值好像也是没问题的。不会出现数据错乱和滑出屏幕再滑回来时从复用池取出cell
后赋值时数据消失的问题。)
输入长度的限制:
需求中要求“网店名称”、“网店主标签”、“网店简介”、“网店座机”都有输入长度的限制,分别为30、20、500、15字数的限制。其实我们在上面初始化数据源的时候已经为每行的数据源model
设置过字数限制了,即maxInputLength
属性。
我们还是以CreateShopTFCell
为例。
要在开始输入的时候监听输入的长度,若字数超过最大限制,则要出现红框,并且显示提示信息。那我们就得给textField
开始输入时添加valueChange
的观察,在textField
输入结束时移除观察。
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 | - (void)textFieldDidEndEditing:(UITextField *)textField { [self clearNotification]; } - (void)textFiledEditChanged:(NSNotification *)obj { UITextField *textField = (UITextField *)obj.object; NSString *toBeString = textField.text; [_formModel setValue:textField.text forKey:_createModel.key]; // 将textField中的值赋给_formModel if(_createModel.maxInputLength&&toBeString.length>0&&toBeString.length>_createModel.maxInputLength){ _bgView.layer.borderColor = HexColor(0xcc2929).CGColor; _checkTipLab.text = [NSString stringWithFormat:@"最多%d个字",(int)_createModel.maxInputLength]; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } if([_createModel.key isEqualToString:@"contactMail"]){ _createModel.errText = @""; }else{ NSString *valueStr = [_formModel valueForKey:_createModel.key]; if(valueStr.length>0){ _createModel.errText = @""; } } } -(void)addNotification { [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(textFiledEditChanged:) name:@"UITextFieldTextDidChangeNotification" object:nil]; } -(void)clearNotification{ [[NSNotificationCenter defaultCenter]removeObserver:self name:@"UITextFieldTextDidChangeNotification" object:nil]; } |
另外,可以看到在textField
开始输入的回调方法里,调用了该cell
的代理方法。该cell
为什么要调用这个代理方法,它需要代理给别人来干什么?…其实这个和键盘遮挡的处理有关,下面我们慢慢解释。
处理键盘遮挡问题:
这个界面有很多行输入框,在自然情况下,下面的几个输入框肯定是在键盘弹出后高度之下的,也即会被键盘遮挡住,我们没法输入。这时就一定处理键盘遮挡问题了。
关于键盘遮挡问题,其实我在以前的一篇笔记中就写过了:UITextField一箩筐——输入长度限制、自定义placeholder、键盘遮挡问题
我们要处理键盘遮挡问题,也就是要实现当键盘弹出时,被遮挡住的输入框能上移到键盘高度之上;当键盘收回时,输入框又能移回原来的位置。那么首先第一步,我们得能获取到键盘弹出或者收回这个动作的时机,在这个时机我们再按需要移动输入框的位置。系统提供了表示键盘弹出和收回的两个观察的key
,分别为UIKeyboardWillShowNotification
和UIKeyboardWillHideNotification
。注册这两个观察者,然后在两者的回调方法里实现输入框位移就大功告成了。
因为键盘遮挡的处理有可能是比较普遍的需求,所以在公司的项目架构设计里是把上面两个关于键盘的观察是注册在APPDelegate.m
中的,并定义了一个有关键盘遮挡处理的协议,协议里定义了一个方法。具体需要具体处理,由需要处理键盘遮挡问题的控制器来实现该协议方法,具体实现怎么移动界面元素来使键盘不遮挡输入框。这么说现在CreateShopViewController
控制器需要处理键盘遮挡问题,那么就需要设置它为APPDelegate
的代理,并由它实现所定义的协议吗?其实不用,公司项目所有的控制器都是继承于基类CommonViewController
,在基类中实现了比较基本和普遍的功能,其实在基类中便定义了下面的方法来设置控制器为APPDelegate
的代理,不过需要属性isListensKeyboard
为YES
。下面这个方法在CommonViewController
中是在viewWillAppear:
方法中调用的。那我们在子类CreateShopViewController
中需要做的仅仅只要在viewWillAppear
之前设置isListensKeyboard
属性为YES
,便会自动设置将自己设为APPDelegate
的代理。然后在CreateShopViewController
控制器里实现协议所定义的方法,实现具体的输入框移动问题。
CommonViewController.m
1 2 3 4 5 6 7 8 9 10 11 12 | -(void)initListensKeyboardNotificationDelegate { if (!self.isListensKeyboard) { return; } if (!self.appDelegate) { self.appDelegate=(AppDelegate*)[[UIApplication sharedApplication] delegate]; } [self.appDelegate setKeyboardDelegate:self]; } |
CreateShopViewController.m
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 | #pragma mark - keyboard delegate - (void)keyboardChangeStatus:(KeyboardChangeType)changeType beginFrame:(CGRect)beginFrame endFrame:(CGRect)endFrame duration:(CGFloat)duration userInfo:(NSDictionary *)info { if(changeType == KeyboardWillShow) { CGFloat keyBoard_h = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; CGFloat newSizeh = _tableView.contentSize.height + keyBoard_h; [UIView animateWithDuration:duration animations:^{ [_tableView setContentSize:CGSizeMake(PDWidth_mainScreen, newSizeh)]; CGFloat set_y = _inputY+50.f+keyBoard_h-_tableView.bounds.size.height; if(set_y>0){ [_tableView setContentOffset:CGPointMake(0, set_y)]; } }]; } else if(changeType == KeyboardWillHide) { [UIView animateWithDuration:duration animations:^{ [_tableView setContentSize:CGSizeMake(PDWidth_mainScreen, _tableView.contentSize.height)]; }]; } } |
可以看到在该代理方法的实现里。当键盘弹出时,我们首先将tableView
的contentSize
在原来的基础上增加了键盘的高度keyBoard_h
。然后将tableView
的contentOffset
值变为set_y
,这个set_y
的值是通过计算而来,但是计算它的_inputY
这个变量代表什么意思?
我们可以回过头去看看tableView
返回cell
的代理方法中,当为CreateShopTFCell
时,我们设置了当前控制器为其cell
的代理。
1 | cell.cellDelegate = self; |
并且我们的控制器CreateShopViewController
也实现了该cell
的协议CreateShopTFCellDelegate
,并且也实现了协议定义的方法。
1 2 3 4 5 | #pragma mark - tfCell delegate - (void)cellBeginInputviewY:(CGFloat)orginY { _inputY = orginY; } |
原来上面的_intputY
变量就是该协议方法从cell
里的调用处传递而来的orginY
参数值。我们回过头看上面的代码,该协议方法是在textField
的开始输入的回调方法里调用的,给协议方法传入的参数是self.frame.origin.y
,即被点击的textField
在手机屏幕内所在的Y
坐标值。
可以看到,处理键盘遮挡问题,其实也不是改变输入框的坐标位置,而是变动tableView
的contentSize
和contentOffset
属性。
选取地址的实现:
CreateShopPickCell
实现里地址的选取和显示。有左右两个框框,点击任何一个将会从屏幕下方弹出一个选取器,选取器有“市”和“区”两列数据对应两个框框,选取器左上方是“取消”按钮,右上方是“确定”按钮。点击“取消”,选取器弹回,并不进行选取;点击“确定”,选取器弹回,选取选择的数据。

CreateShopPickCell
的界面元素布局没什么可说的,值得一说的是弹出的pickView
视图,是在cell
的填充数据的方法中创建的。
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 | - (void)refreshContent:(CreateShopModel *)createModel formModel:(CreateShopFormModel *)formModel; { [self clearCellData]; if(!createModel){ return; } [self createPickerView]; // 创建pickView _createModel = createModel; _formModel = formModel; _titleLab.text = createModel.title; if(formModel.regionId.length>0){ ShopAddressModel *area=[[ShopAddressModel alloc]init]; area.addresssId=formModel.regionId; [_pickView dafaultArea:area]; }else{ _cityLab.text = @"市"; _cityLab.textColor = HexColor(0xc8c8c8); _areaLab.text = @"区"; _areaLab.textColor = HexColor(0xc8c8c8); } if(createModel.errText.length>0){ _cityBgView.layer.borderColor = HexColor(0xcc2929).CGColor; _areaBgView.layer.borderColor = HexColor(0xcc2929).CGColor; _checkTipLab.text = createModel.errText; }else{ _cityBgView.layer.borderColor = PDColor_Dividing_line.CGColor; _areaBgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } } |
这里只是创建了pickView
的对象,并设置了数据源items
,已经点击之后的回调block
,而并未将其添加在父视图上。
要将选取的“市&区”的结果从CustomPickView
中以block
回调到cell
来,将数据赋给_formModel
。并且当有了数据后UILabel
的文本颜色也有变化。
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 | -(void)createPickerView { if (!_pickView) { _pickView= [[CustomPickView alloc] init]; } [_pickView setItems:[ShopAddressModel cityAddressArr]]; [_pickView SelectedBlock:^(ShopAddressModel *city, ShopAddressModel *area) { if (city) { [_formModel setValue:city.addresssId forKey:_createModel.key]; _cityLab.text = city.name; _cityLab.textColor = PDColor_Title_Black; } if (area) { [_formModel setValue:area.addresssId forKey:_createModel.key]; _areaLab.text = area.name; _areaLab.textColor = PDColor_Title_Black; } if(city){ _cityBgView.layer.borderColor = PDColor_Dividing_line.CGColor; _areaBgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; _createModel.errText=@""; }else{ _cityBgView.layer.borderColor = HexColor(0xcc2929).CGColor; _areaBgView.layer.borderColor = HexColor(0xcc2929).CGColor; _checkTipLab.text = _createModel.errText; } }]; } |
pickView
的对象已经创建好,但是还未到弹出显示的时机。所谓时机,就是当左右两个框框被点击后。
可以看到pickView
是被添加在window
上的。并且调用了pickView
的接口方法showPickerView
方法,让其从屏幕底部弹出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | - (void)cityGestureHandle:(UITapGestureRecognizer *)tapGesture { [_superView endEditing:YES]; [self showPicker]; } - (void)areaGestureHandle:(UITapGestureRecognizer *)tapGesture { [_superView endEditing:YES]; [self showPicker]; } -(void)showPicker { [[PubicClassMethod getCurrentWindow] addSubview:_pickView]; [_pickView showPickerView]; } |
前面代码中给pickView
设置数据源时,它的数据源有点特别,调用了ShopAddressModel
的类方法cityAddressArr
来返回有关地址的数据源数组。这是因为这里的地址数据虽然是从服务器接口请求的,但是一般情况不会改变,最好是从服务器拿到数据后缓存在本地,当请求失败或者无网络时仍不受影响。
ShopAddressModel
类定义了如下几个属性和方法。
1 2 3 4 5 6 7 8 9 10 11 12 | @interface ShopAddressModel : NSObject @property (nonatomic, copy)NSString *addresssId; @property (nonatomic, copy)NSString *name; @property (nonatomic, strong)NSArray *subArr; #pragma mark - 地址缓存 + (void)saveAddressArr:(NSArray *)addressArr; +(NSArray*)cityAddressArr; +(NSArray*)addressArr; #pragma mark - 解析 + (ShopAddressModel *)addressModelFromDict:(NSDictionary *)dict; @end |
当我们我们从服务器拿到返回而来的地址数据后,调用saveAddressArr:
方法,将数据缓存在本地。
1 2 3 4 5 6 7 8 9 10 11 12 13 | + (void)saveAddressArr:(NSArray *)addressArr { if (addressArr && addressArr.count > 0) { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:addressArr]; [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"saveAddressArr"]; }else { [[NSUserDefaults standardUserDefaults]setObject:nil forKey:@"saveAddressArr"]; } [[NSUserDefaults standardUserDefaults] synchronize]; } |
当创建好pickView
后以下面方法将本地缓存数据读出,赋给items
作为数据源。
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 | +(NSArray*)cityAddressArr { NSArray *arr=[ShopAddressModel addressArr]; ShopAddressModel *pro=[arr firstObject]; if (pro.subArr.count>0) { return pro.subArr; } return nil; } +(NSArray*)addressArr { NSData *data = [[NSUserDefaults standardUserDefaults] valueForKey:@"saveAddressArr"]; NSArray *addrssArr=[NSKeyedUnarchiver unarchiveObjectWithData:data]; if(addrssArr.count==0) { return nil; } NSMutableArray *areas=[[NSMutableArray alloc]init]; for (int i=0; i0) {//市 city.subArr=[NSArray arrayWithArray:aArr]; } [cArr addObject:city]; } if (cArr.count>0) {//省 prov.subArr=[NSArray arrayWithArray:cArr]; } [areas addObject:prov]; } return areas; } |
注意:这也是为什么把创建pickView
的代码放在了填充cell
数据的refreshContent:formModel:
里,而不在创建cell
界面元素时一气创建pickView
。因为那样当用户第一次打开这个界面,有可能数据来的比较慢,当代码执行到赋数据源items
时,本地还没有被缓存上数据呢!这样用户第一次进入这个界面时弹出的pickView
是空的,没有数据。而放在refreshContent:formModel:
中是安全稳妥的原因是,每次从接口拿到数据后我们会刷新tableView
,便会执行refreshContent:formModel:
方法。它能保证先拿到数据,再设置数据源的顺序。
提交表单时校验数据:
在将表单数据提交前,要先校验所填写的表单是否有问题,该填的是否都填了,已填的数据格式是否是对的。若有问题,则要出现红框和提示信息提醒用户完善,等数据无误后才可以提交给服务器。
数据校验代码很繁长,写在控制器里不太好。因为它是对表单数据的校验,那我们就写在CreateShopFormModel
里,这样既可以给控制器瘦身,也可以降低耦合度,数据的归数据,逻辑的归逻辑。
从前面CreateShopFormModel.h
的代码里我们其实已经看到了这个校验方法:submitCheck:
。若某条CreateShopFormModel
实例的数据不达要求,则在相应的CreateShopModel
数据源对象的errText
属性赋值,意为提示信息。该方法的返回值类型为BOOL
值,有数据不合格则返回NO
。此时,在调用该方法的外部,应该将tableView
重新加载,因为此时在该方法内部,已将数据格式不合格的提示信息赋值给了相应的数据源model
。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | - (BOOL)submitCheck:(NSArray*)dataArr { BOOL isSubmit=YES; if(self.groupName.length==0){ if (dataArr.count>0) { CreateShopModel *cellObj=dataArr[0]; cellObj.errText=@"网店名不能为空"; } isSubmit=NO; } if(self.groupName.length>0){ if(dataArr.count>0){ if(self.groupName.length>30){ CreateShopModel *cellObj=dataArr[0]; cellObj.errText=@"最多30个字"; isSubmit=NO; } } } if(self.tag.length==0){ if (dataArr.count>1) { CreateShopModel *cellObj=dataArr[1]; cellObj.errText=@"标签不能为空"; } isSubmit=NO; } if(self.introduction.length==0){ if (dataArr.count>2) { CreateShopModel *cellObj=dataArr[2]; cellObj.errText=@"简介不能为空"; } isSubmit=NO; } if(self.introduction.length>0){ if(dataArr.count>2){ if(self.introduction.length>30){ CreateShopModel *cellObj=dataArr[2]; cellObj.errText=@"最多500个字"; isSubmit=NO; } } } if(self.regionId.length==0){ if (dataArr.count>3) { CreateShopModel *cellObj=dataArr[3]; cellObj.errText=@"市区不能为空"; } isSubmit=NO; } if(self.address.length==0){ if (dataArr.count>4) { CreateShopModel *cellObj=dataArr[4]; cellObj.errText=@"地址不能为空"; } isSubmit=NO; } if(self.telephone.length==0){ if (dataArr.count>5) { CreateShopModel *cellObj=dataArr[5]; cellObj.errText=@"电话不能为空"; } isSubmit=NO; } if (self.contactMail.length>0) { if (dataArr.count>6) { CreateShopModel *cellObj=dataArr[6]; if(![PubicClassMethod isValidateEmail:self.contactMail]){ cellObj.errText=@"邮箱格式不合法"; isSubmit=NO; } } } if(self.logoUrl.length==0&&!self.logo){ if (dataArr.count>7) { CreateShopModel *cellObj=dataArr[7]; cellObj.errText=@"logo不能为空"; } isSubmit=NO; } if(self.coverUrl.length==0&&!self.cover){ if (dataArr.count>8) { CreateShopModel *cellObj=dataArr[8]; cellObj.errText=@"封面图不能为空"; } isSubmit=NO; } return isSubmit; } |
上传图片到七牛:
当点击了“提交”按钮后,先校验数据,若所填写的数据不合格,则给出提示信息,让用户继续完善数据;若数据无问题,校验通过,则开始提交表单。但是,这里有图片,图片我们是上传到七牛服务器的,提交表单是图片项提交的应该是图片在七牛的一个url
。这个逻辑我在以前的这篇笔记已经捋过了APP上传图片至七牛的逻辑梳理。
但是当时所有的逻辑都是写在控制器里的。我们这个“创建网店”的控制器已经很庞大了,写在控制器里不太好。所以在这里我将上传图片的逻辑拆分了出去,新建了一个类`QNUploadPicManager
。只暴露一个允许传入UIImage
参数的接口方法,便可以通过successBlock
来返回上传到七牛成功后的url
。以及通过failureBlock
来返回上传失败后的error
信息。而将所有的逻辑封装在QNUploadPicManager
内部,这样控制器里便精简了不少代码,清爽了许多。
QNUploadPicManager.h
1 2 3 4 5 | @interface QNUploadPicManager : NSObject - (void)uploadImage:(UIImage *)image successBlock:(void(^)(NSString *urlStr))successBlock failureBlock:(void(^)(NSError *error))failureBlock; @end |
QNUploadPicManager.m
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | #import "QNUploadManager.h" #define kImageFilePath(name) [NSTemporaryDirectory() stringByAppendingPathComponent:name] // 图片路径 @implementation QNUploadPicManager - (void)uploadImage:(UIImage *)image successBlock:(void(^)(NSString *urlStr))successBlock failureBlock:(void(^)(NSError *error))failureBlock { NSString *logoFileName = [self fileNameWithPicture:image]; // fileName [self requestUploadToken:logoFileName successBlock:^(NSDictionary *dict) { [self uploadPicOnQNParameters:dict fileName:logoFileName complete:^(NSString *key, NSDictionary *resp) { [self getPictureUrlOnQN:key successBlock:^(NSString *urlStr) { successBlock(urlStr); // 成功回调 } failure:^(NSError *error) { failureBlock(error); }]; }]; } failure:^(NSError *error) { failureBlock(error); // token获取失败回调 }]; } // get token - (void)requestUploadToken:(NSString *)fileName successBlock:(void(^)(NSDictionary *dict))successBlock failure:(void(^)(NSError *error))failureBlock { NSDictionary * parameters=[[NSDictionary alloc] initWithObjectsAndKeys: @(1), @"count", nil]; NSString *url = [NSString stringWithFormat:@"%@/cbs/%@/upload/token",HTTPSURLEVER,Interface_Version]; url =[url stringByReplacingOccurrencesOfString:@"http" withString:@"https"]; AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager]; [mgr.requestSerializer setValue:[[REDUserModel shareInstance] token] forHTTPHeaderField:@"x-auth-token"]; [mgr.requestSerializer setValue:@"ios" forHTTPHeaderField:@"_c"]; [mgr.securityPolicy setAllowInvalidCertificates:YES]; [mgr GET:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) { // key:5425734430926807040 successBlock(DealWithJSONValue(responseObject[@"b"][0])); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { failureBlock(error); }]; } // upload on QN - (void)uploadPicOnQNParameters:(NSDictionary *)parameters fileName:(NSString *)fileName complete:(void(^)(NSString *key, NSDictionary *resp))complete { QNUploadManager *uploader = [[QNUploadManager alloc] init]; // 异步多线程 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *token = parameters[@"uploadToken"]; NSString *key = parameters[@"key"]; [uploader putFile:kImageFilePath(fileName) key:key token:token complete:^(QNResponseInfo *info, NSString *key, NSDictionary *resp) { // key:5425734430926807040 complete(key, resp); } option:nil]; }); } //获取上传图片的url - (void)getPictureUrlOnQN:(NSString *)token successBlock:(void(^)(NSString *urlStr))successBlock failure:(void(^)(NSError *error))failureBlock { if(!token){ token = @""; } NSDictionary * parameters=[[NSDictionary alloc] initWithObjectsAndKeys: token, @"token", nil]; NSString *url = [NSString stringWithFormat:@"%@/cbs/%@/upload/url",HTTPSURLEVER,Interface_Version]; url =[url stringByReplacingOccurrencesOfString:@"http" withString:@"https"]; AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager]; [mgr.requestSerializer setValue:[[REDUserModel shareInstance] token] forHTTPHeaderField:@"x-auth-token"]; [mgr.requestSerializer setValue:@"ios" forHTTPHeaderField:@"_c"]; [mgr.securityPolicy setAllowInvalidCertificates:YES]; [mgr GET:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) { if (!responseObject[@"b"] || [responseObject[@"b"] isEqual:[NSNull null]]) { return; } successBlock(DealWithJSONStringValue(responseObject[@"b"][@"url"])); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { failureBlock(error); }]; } // save in file - (NSString *)fileNameWithPicture:(UIImage *)image { if(!image){ return @""; } UIImage *newImage = [PubicClassMethod imageWithImageSimple:image scaledToSize:CGSizeMake(80, 80)]; // 图片压缩 NSData *imageData = UIImageJPEGRepresentation(newImage, 1); NSString *fileName = [NSString stringWithFormat:@"%d.png",arc4random()]; BOOL isWrited = [imageData writeToFile:kImageFilePath(fileName) atomically:YES]; if(isWrited){ return fileName; } return @""; } @end |
总结:
这个界面比较核心的一个问题就是:要在控制器里提交表单,那怎样把在UITableViewCell
里的textField
输入的数据传递给控制器? 另外一个问题是一个逻辑比较复杂的界面,控制器势必会很庞大,应该有意的给控制器瘦身,不能把所有的逻辑都写在控制器里。有关视图显示的就考虑放入UITableViewCell
,有关数据的就考虑放入model
。这样既为控制器瘦身,也使代码职责变清晰,耦合度降低。
另外,今天2016最后一天班了,周日就坐车回家过年了。提前祝各位新春快乐。
相关文章:

2014计算机三级网络技术,2014计算机三级网络技术综合题解题思路
2014计算机三级网络技术综合题解题思路,全部自码第一小题 IP地址的计算公式正常IP地址计算:已知IP地址;子网掩码;地址类别:A类地址:1—126(00)B类地址:128—191(10)C类地址:192—223(110) D类地…

word 生成HTML
View Code 1 string wordPath Server.MapPath("/Fileword/" FileUpload1.FileName); 2 string htmlPath Server.MapPath("/Fileword/测试.html");3 //上传word文件4 FileUpload1.SaveAs(wordP…

CCF系列之画图(201409-2)
试题编号: 201409-2试题名称: 画图时间限制: 1.0s内存限制: 256.0MB问题描述: 问题描述在一个定义了直角坐标系的纸上,画一个(x1,y1)到(x2,y2)的矩形指将横坐标范围从x1到x2,纵坐标范围从y1到y2…

大连理工计算机专业导师,大连理工大学计算机科学与技术学院研究生导师简介-申彦明...
大连理工大学计算机科学与技术学院研究生导师简介-申彦明大连理工大学 免费考研网/2016-05-04申彦明院系:计算机科学与技术学院办公电话:无电子信箱:shendlut.edu.cn更新时间:2014-4-4其他专业:计算机系统结构个人简介…
iOS基础问答面试题连载-附答案
2017-02-02 timhbw CocoaChina以下是一些自己收集的比较基础的问题(大神可以忽略),附上答案,方便大家阅读。俗话说得好,基础不牢,地动山摇。文章末尾会提供PDF版的文档,方便大家木有网的时候也可…

一个非常简单的 ASP.NET MVC 示例:长轮询(又叫:反向 AJAX,英文名:Comet)实现...
关于 长轮询(又叫:反向 AJAX,英文名:Comet)的介绍,请查看:反向Ajax,第1部分:Comet介绍 下面是代码实现: UI: <p><input type"button" onc…

周记 2016.4.5
1. BUILD_IDpleaseDontKillMe /usr/local/tomcat_car/bin/shutdown.sh /usr/local/tomcat_car/bin/startup.sh 2. windown中mysql解压版设置密码: 最开始mysql没有密码,启动mysql后: cmd -- mysql -u root ----- use mysql , 然后执行下面命令 update…

西北工业大学21计算机考研,西北工业大学2018年计算机考研879专业综合考试大纲...
题号:879《专业综合》考试大纲《专业综合》含数据结构、计算机网络、计算机组成原理、信号与系统四部分组成,四选二。一、数据结构1. 数据结构、抽象数据类型的概念;2. 线性结构的相关内容。通用线性表和特殊线性表(栈、队列、广义表等)的逻辑结构以及物…
iOS 实现多个可变 cell 复杂界面的制作
来源:飘游人 www.jianshu.com/p/9fc838d46f5e 如有好文章投稿,请点击 → 这里了解详情 在日常的开发中,有时会遇到内容块比较多,且又可变的界面: 这个界面中有些内容块是固定出现的,比如最上面的商品详情图…

做销售如何跟单,逼单!共20招!(转)
逼单是整个销售业务过程中最重要的一个环节。如果逼单失败你的整个业务就会失败,其实整个业务过程就是一个“逼”的过程,逼要掌握技巧,不要太操之过急,也不要慢条斯理,应该张弛有度,步步为营,也…

how to write Makefile
http://www.cnblogs.com/aoyihuashao/archive/2010/01/18/1650865.html转载于:https://www.cnblogs.com/hSheng/archive/2013/03/27/2985145.html

职称计算机word2007难吗,职称计算机word2007原题
全国职称计算机考试题库——word2007模块word2007模块第1套试题1.修改当前文本窗体类型为"数字",默认数字为"5"。2.设置用户在键入“中兴瑞典”时自动替换为“中星睿典”。3.将当前图片位置改为四周型环绕。4.用WORD的插入技术,插入如下字符:④…
iOS 9 通用链接(Universal Links)
来源:iOS_小松哥 www.jianshu.com/p/734c3eff8feb 如有好文章投稿,请点击 → 这里了解详情 什么是Universal Links? 在iOS9之前,对于从各种从浏览器、Safari中唤醒APP的需求,我们通常只能使用scheme。但是这种方式需要提前判断系…
为什么你的工作经验不值钱
前言 每年的三月到六月,都是招聘高峰,除了大量的应届毕业生涌入社会之外,还有一些工作了一两年尚未找到稳定归属感的人,也会开始投递简历,是的,基本都是在拿了年终奖之后。作为前端技术主管,有幸…

http://bbs.phome.net/showthread-13-45519-0.html
适用于想使用EmpireCMS建站的用户;EmpireCMS零基础的用户;会使用Dreamweaver设计简单表格式模板的用户。教程在线观看: 安装实例教程: http://www.phome.net/doc/ecmsedu/rm/install.htm入门实例教程: http://www.phom…

计算机32位操作系统最大识别到内存,win7 32位系统可以支持多大的内存_win7 的32位系统最大支持多少g的内存...
大家都知道win7系统有32位和64位之分,而两者的安装配置是不一样的,一般4G内存的我们安装64位的系统,但是很多用户不知道win7 32位系统可以支持多大的内存,这就给大家分享一下win7 的32位系统最大支持多少g的内存吧。一、内存和操作…
33个2017年必须了解的iOS开源库
原文 本文翻译自Medium,原作者为 Paweł Białecki 照片版权:(Unsplash/Markus Pe) 你好,iOS 开发者们!我的名字叫 Paweł,我是一个独立 iOS 开发者,并且是 Enter Universe 的作者。…

codeforces 610D D. Vika and Segments(离散化+线段树+扫描线算法)
题目链接: D. Vika and Segments time limit per test2 secondsmemory limit per test256 megabytesinputstandard inputoutputstandard outputVika has an infinite sheet of squared paper. Initially all squares are white. She introduced a two-dimensional c…

ubuntu下安装redis
安装reids服务器 apt-get install redis-server 测试是否安装成功 redis-cli 安装phpredis扩展 #wgethttps://github.com/nicolasff/phpredis/downloads #tar -zxvf nicolasff-phpredis-2.1.3-124-gd4ad907.tar.gz # mv nicolasff-phpredis-d4ad907 php-5.3.8/ext/phpredis/ # …

往往存储与计算机硬盘或其他,硬盘是计算机系统中信息资源最重要的存储设备其所存放信息-Read.DOC...
硬盘是计算机系统中信息资源最重要的存储设备其所存放信息-ReadPAGEPAGE 2摘要关键字:磁盘、硬盘、中断13、扩展中断13、分区表、MBR、DBR、DPT、Boot、CMOS、FAT、柱面、磁道、磁头、扇区随着科学技术的不断发展和社会信息化程度的不断提高,电脑已逐渐深…
【Ghost Blog】如何给Ghost Blog添加背景音乐
昨天闲着无聊,就给自己的电脑装了一个Ghost的博客,打开博客的第一眼就被震撼到了,我们可以发现界面十分的简介。。。。上面的都是废话 我们来看一看我我选择的音乐播放器——网易云音乐,这个播放器就是在一个歌曲上点开之后有一个…
AE 动画直接变原生代码:Airbnb 发布开源动画库 Lottie
原文 Airbnb 发布的 Lottie 是一个面向 iOS、Android 和 React Native 的开源动画库。 简单来说,就是可以直接利用 AE 导出的 JSON 动画文件,将其解析为原生代码,并跨平台运行在设备上。 根据身边朋友的试用,通过 Canvas 绘制动画…

纹理贴图的模式设置
1 要对纹理进行任何的操作,必须先使该纹理问当前的active纹理 glGenTextures( 1, &reflectionTexObj );glBindTexture( GL_TEXTURE_2D, reflectionTexObj );glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );glTexParameteri( GL_TEXTURE_2D, G…

人社局计算机考试报名时间,内蒙古人社局:2016年下半年计算机软件水平考试报名时间通知...
关于做好2016年度下半年计算机技术与软件专业技术资格、翻译专业资格(水平)考试笔译考试报名工作的通知各旗县区人力资源和社会保障局、市直有关单位:根据内蒙古自治区人事考试中心《关于做好2016年度下半年计算机技术与软件专业技术资格(水平)考试报名工作的通知》…
即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket)
来源:涂耀辉 www.jianshu.com/p/2e16572c9ddc 如有好文章投稿,请点击 → 这里了解详情 前言 本文旨以实例的方式,使用CocoaAsyncSocket这个框架进行数据封包和拆包。来解决频繁的数据发送下,导致的数据粘包、以及较大数据&#x…

linux下 为自己编写的程序 添加tab自动补全 功能
linux下 为自己编写的程序 添加tab自动补全功能 入门 complete 在我的tmp下随便写了一个a.sh, 为他补全edit /etc/bash_completion.d/foo_foo() {local cur prev optsCOMPREPLY()cur"${COMP_WORDS[COMP_CWORD]}"prev"${COMP_WORDS[COMP_CWORD-1]}"opts&quo…

笔记本电脑(Windows7)实现无线AP
使用环境:出差两个同事住一个房间、网线不够用、没有路由器 1、在windows命令窗口中运行以下命令 netsh wlan set hostednetwork modeallow netsh wlan set hostednetwork ssidOPEN key1234567890 netsh wlan start hostednetwork 命令解释:在笔记本插有…

华北电力大学计算机图形学实验报告,华北电力大学计算机图形学实验报告分析.doc...
华北电力大学计算机图形学实验报告分析科 技 学 院课程设计(综合实验)报告( 2013 -- 2014 年度第 2 学期)实验名称 OpenGL基本图元绘制实验课程名称 计算机图形学||专业班级:计算机11K1 学生姓名:曲强学 号:111909010118 成 绩:指…
Fastlane 入门实战教程从打包到上传iTunes connect
有关神器 Fastlane 持续集成\部署的文章网上挺多,本文定位是入门教程,针对 iOS 应用的持续部署,只需一条命令就可实现从 Xcode 项目到 编译\打包\构建\提交审核 文章稍微有点长,涵盖内容为:fastlane 简介\安装\配置 Snapshot 截图 XCTest 一键上传App Store 说明:本文将 App…

double int char 数据类型
贴心的limits... 测试代码: #include <iostream> #include <stdio.h> #include <limits> #include <math.h> using namespace std;int main() {//double 有效数字16位double test3 1.2345678912345678e17;printf("%.17lf\n", te…