前言
這幾天專案的新需求中有個複雜的表單介面,在做的過程中發現要比想象中複雜很多,有好多問題需要處理。有很多東西值得寫下來好好梳理下。
需求分析:
上圖便是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最後一天班了,週日就坐車回家過年了。提前祝各位新春快樂。