“不為炫技而炫技,不為架構而設計架構,只為寫出一個接地氣、通俗易懂的使用方法”
(注:瞭解整個設計模式體系請檢視我上篇文章實際專案中的MVVM(積木)模式:序章)
這篇文章講解Model和ViewModel。
本著易擴充套件、易理解的前提,講解中Model和ViewModel都用最基礎的方法和易理解的思維圖。
為何為何會將View和ViewModel合併在一起講解?為何我會將有些文章說的的網路層與資料層合併一起叫資料層?
很簡單,我們需要明白一個道理,無論是網路請求還是本地快取,本質上都是傳遞資料;因此,我們要做的就是將各種來源的資料通過資料加工(ViewModel)形成統一的格式(Model)再通過一個統一的介面傳遞給需要的地方。我所講的資料層,我把這形象地叫為資料工廠。
首先,我們先開始說說:Model。
一、Model–專案的資訊承載與傳遞者
一個多人協同開發的專案,保證資料結構的一致性和穩定是很有必要的。而Model則是很好的實現了這一需求。
首先,我們先通過三個大的方面將字典與Model作一個比較,更直觀瞭解Model的特點。
1、字典與模型的比較
a、取值:字典會因為沒有取值的這個key或者這個錯誤的key剛好是這個字典中其他型別的值對應的key(因輸入錯誤等原因),通過這個key取出來的值為nil或者其他型別的值,如賦值給label之類的文字控制元件,可能會導致程式崩潰,而Model不會出現這樣的問題;
b、資料展示:字典無法再不改變資料來源的前提下,改變資料的格式,而Model則可以通過get方法實現這個需求,保證了資料的原始性和可變性;
c、後期維護:字典每個key的具體含義和有多少key要通過介面文件去了解,而Model體現在具體的屬性和每個屬性的備註上;
可能有同學會問建立Model一個個去複製貼上屬性好麻煩,還有像資料快取之類還要一個個寫解擋歸檔好麻煩呢/(ㄒoㄒ)/~~
這麼多好的優點的前提下,這幾個小麻煩肯定會通過方法解決噻,且看下面:
2、解決Model的一些小麻煩
a、如何快速建立Model:這裡有份程式碼,可以將網路請求下來的字典裡的key在控制檯列印成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 |
- (void)writeInfoWithDict:(NSDictionary *)dict { NSMutableString *strM = [NSMutableString string]; [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { // NSLog(@"%@,%@",key,[obj class]); NSString *className = NSStringFromClass([obj class]) ; if ([className isEqualToString:@"__NSCFString"] | [className isEqualToString:@"__NSCFConstantString"] | [className isEqualToString:@"NSTaggedPointerString"]) { [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",key]; }else if ([className isEqualToString:@"__NSCFArray"] | [className isEqualToString:@"__NSArray0"] | [className isEqualToString:@"__NSArrayI"]){ [strM appendFormat:@"@property (nonatomic, strong) NSArray *%@;\\\\n",key]; }else if ([className isEqualToString:@"__NSCFDictionary"]){ [strM appendFormat:@"@property (nonatomic, strong) NSDictionary *%@;\\\\n",key]; }else if ([className isEqualToString:@"__NSCFNumber"]){ [strM appendFormat:@"@property (nonatomic, copy) NSNumber *%@;\\\\n",key]; }else if ([className isEqualToString:@"__NSCFBoolean"]){ [strM appendFormat:@"@property (nonatomic, assign) BOOL %@;\\\\n",key]; }else if ([className isEqualToString:@"NSDecimalNumber"]){ [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",[NSString stringWithFormat:@"%@",key]]; } else if ([className isEqualToString:@"NSNull"]){ [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",[NSString stringWithFormat:@"%@",key]]; }else if ([className isEqualToString:@"__NSArrayM"]){ [strM appendFormat:@"@property (nonatomic, strong) NSMutableArray *%@;\\\\n",[NSString stringWithFormat:@"%@",key]]; } }]; NSLog(@"\\\\n\\\\n%@\\\\n",strM); } |
看下圖列印效果,那麼列印出來的效果大家知道了吧,直接複製貼上就OK啦:
b、怎麼解決繁瑣的解擋歸檔呢
在BaseModel(Model的基類)中寫一個統一的解擋、歸檔,這裡就要用到runtime中非常有用的兩個方法:
1 |
class_copyIvarList(Class cls, unsigned int *outCount) //遍歷該類成員變數列表 |
1 |
ivar_getName(Ivar v) //獲取該類某個成員變數的名字 |
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 |
- (void)encodeWithCoder:(NSCoder *)encoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); // 歸檔 NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; [encoder encodeObject:value forKey:key]; } free(ivars); } - (id)initWithCoder:(NSCoder *)decoder { if (self = [super init]) { unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); // 解檔 NSString *key = [NSString stringWithUTF8String:name]; id value = [decoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivars); } return self; } |
至此,所有繼承於這個Model基類的Model都自動實現瞭解檔歸檔的方法。
既然解決了建立Model的一些小麻煩,我們就來構建一個專案中標準的BaseModel(基類模型)。
3、何為基類Model建立要求?
a、資料格式讀取統一與寫入統一;
b、模型屬性值可批量修改;
這樣才能保證在“千奇百怪、朝令夕改”的資料來源中,進入到這個專案體系後,面向業務工程師的時候,是統一整齊的標準模型,然後業務工程師才會在這個基礎之上擴充套件其他子Model。
其中讀寫統一則是通過上面的解檔歸檔解決,而模型屬性值批量修改則是通過李明傑大神的MJExtension(這個三方庫可以在不用繼承其他Model前提下使用,保證了Model獨立性),具體程式碼在BaseModel如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property { if ([property.name isEqualToString:@"buy_price"]|| [property.name isEqualToString:@"buy_sale_price"]||[property.name isEqualToString:@"info_price"]||[property.name isEqualToString:@"sale_price"] ) { if (oldValue == nil || [oldValue isKindOfClass:[NSNull class]]) return @"價格面議"; }//可將所有模型在生成時將涉及相應欄位的值變成“價格面議” else if (property.type.typeClass ==[NSString class]) { if (oldValue == nil || [oldValue isKindOfClass:[NSNull class]]) return @"--";//可將所有模型在生成時將string型別從nil或Null變成“--” } else if (property.type.typeClass == [NSArray class]) { if (oldValue == nil) return [[NSArray alloc] init];//可將所有模型在生成時將array型別從nil變成[[NSArray alloc] init] } return oldValue; } |
以上程式碼舉了部分替換的例子,目的是告訴大家通過這方法可以在資料來源傳給客戶端是非友好資料的時候,我們客戶端能夠進行處理,給業務工程師一個友好的資料。這對資料工廠這個模式來說是非常有必要的。
至此,BaseModel就只有寫兩個方法:解檔與歸檔 以及 屬性值批量修改。因為我們始終要明白:Model是來保證整個專案架構資料結構的一致性和穩定性。那麼繼承這個BaseModel的子Model就都具備了面向業務工程師友好資料的特點。
那麼如何正確使用子Model呢?
我舉兩個例子:
1、如何將模型源資料的時間 20161010 變成 2016-10-10(正確使用get方法)
1 2 3 4 5 |
- (NSString *)pub_date{ return [_pub_date formatDateString];//formatDateString是NSString的Category } |
這樣的好處有兩方面:一方面,業務工程師不會修改源資料,保證了源資料的安全性;另一方面,類似formatDateString的方法,是通過Category(也就是我後面要說的工具類)使用的,保證了程式碼的低耦合性。
2、使用子Model分離資料的一個例子(這個例子感謝我的同事 張爾柏 同學提供,展示這個例子主要目的是為了表達子Model其中一個在cell樣式資料分離的作用,可能會因為沒有demo,大家不太好理解。所以,大家可以在後期demo上傳後再次詳細瞭解)
這裡我們舉個tableView中Cell顯示(整個tableView的demo將會在View篇結束後放出),其中Model要做的事情,我們先看在Model中的程式碼:
1 2 3 4 5 |
- (void)setupInfo { self.xibName = @"SaleInfoTableViewCell";/指定Cell的Cell樣式 self.cellHeight = @(76);//指定Cell的高度 self.ideltifier = @"cell";//指定Cell的ideltifier } |
通過在Model生成時執行這個方法,實現了Cell樣式與樣式資料分離,做到了每個Cell的View樣式與Model的繫結。
在講解Model的結尾,總結起來就是:Model存在的目的是為了給業務工程師一個友好穩定的資料,讓業務工程師在相應的模組內獨立地做相應的資料操作。
二、ViewModel–做一個優秀的資料工廠
如文章開始的圖就知道,ViewModel更像一個食品工廠一樣,將不同的原料通過不同的製作工藝產出為統一的產品。
那麼作為工廠的框架BaseViewModel應該是怎樣的呢?
首先我們應該先想到,我們的原料(資料)來自哪裡?
基本都是來自網路了噻!
既然來自網路,那麼就明確了BaseViewModel應該實現三個事情:網路通訊、上傳、下載。(都用af第三方庫實現)
廢話不多說,直接上程式碼:
1、BaseViewModel實現的三個方法
網路請求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (void)serviceNetWorkWithUrlStr:(NSString *)urlStr//請求網路地址 Params:(NSMutableDictionary *)params//請求引數 Success:(void(^)(id result))successBlock Failure:(void(^)(NSError *error))failBlock { AFHTTPRequestOperationManager * manager = [AFHTTPRequestOperationManager manager]; manager.requestSerializer.timeoutInterval = 10;//請求時長 manager.requestSerializer = [AFHTTPRequestSerializer serializer]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; //如果需要加解密,可引入加解密的工具類將params加密實現 [manager POST:urlStr parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) { successBlock(responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"%@", [error localizedDescription]); failBlock(error); }]; } |
上傳:
該上傳方法需要求能同時多傳,且傳不同型別的檔案,以適應不用的場景需要
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 |
- (void)uploadFileWithfileList:(NSMutableArray *)params//存放上傳檔案的陣列 Option:(NSDictionary *)optiondic//請求引數 Url:(NSString *)requestURL//上傳網路地址 Success:(void(^)(id responseObject))successBlock Failure:(void(^)(NSError *error))failBlock progress:(void (^)(float progress))progress{ AFHTTPRequestOperationManager *manager=[AFHTTPRequestOperationManager manager]; //設定返回的資料解析格式 manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"]; manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/json"]; AFHTTPRequestOperation *operation = [manager POST:requestURL parameters:optiondic constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { //下方舉了一個極端例子:當陣列裡存放了不同型別的檔案如何上傳 for (int i = 0; i<params.count; i++) { if ([params[i] isKindOfClass:[UIImage class]]) { UIImage *image = [params[i] imageCompressForWidth:params[i] targetWidth:375]; NSData *imageData =UIImagePNGRepresentation(image); [formData appendPartWithFileData:imageData name:@"file_content" fileName:[NSString stringWithFormat:@"anyImage_%d.png",i] mimeType:@"image/png"]; }else if ([params[i] isKindOfClass:[NSString class]]) { NSURL *url = [NSURL fileURLWithPath:params[i]]; NSData *data = [NSData dataWithContentsOfURL:url]; [formData appendPartWithFileData:data name:@"file_content" fileName:@"11.aac" mimeType:@"audio/x-mei-aac"]; }else if ([params[i] isKindOfClass:[NSURL class]]) { NSData *data = [NSData dataWithContentsOfURL:params[i]]; [formData appendPartWithFileData:data name:@"file_content" fileName:@"11.aac" mimeType:@"audio/x-mei-aac"]; } } } success:^(AFHTTPRequestOperation *operation, id responseObject) { successBlock(responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { failBlock(error); }]; //獲得上傳進度 [operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) { NSLog(@"百分比:%f",totalBytesWritten*1.0/totalBytesExpectedToWrite); progress(totalBytesWritten*1.0/totalBytesExpectedToWrite); }]; } |
下載:
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 |
- (void)downloadFileWithOption:(NSDictionary *)paramDic//請求引數 withUrl:(NSString*)requestURL//下載地址 savedPath:(NSString*)savedPath//儲存路徑 downloadSuccess:(void (^)(id responseObject))success downloadFailure:(void (^)(NSError *error))failure progress:(void (^)(float progress))progress { AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer]; NSMutableURLRequest *request =[serializer requestWithMethod:@"POST" URLString:requestURL parameters:paramDic error:nil]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]initWithRequest:request]; [operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:savedPath append:NO]]; [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { float p = (float)totalBytesRead / totalBytesExpectedToRead; progress(p);//下載進度 NSLog(@"download:%f", (float)totalBytesRead / totalBytesExpectedToRead); }]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { success(responseObject); NSLog(@"下載成功"); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { failure(error); NSLog(@"下載失敗"); }]; [operation start]; } |
2.如何寫好一個子ViewModel
不知道大家注意到沒有,其實我們真實的專案中,網路請求返回來的狀態其實可能是會存在三種狀態的:請求成功;請求失敗,伺服器返回錯誤資訊;請求失敗,網路不通。同時,我們會在某些地方做快取讀寫。那既然如此,繼承BaseViewModel的子ViewModel的對外介面(唯一對外介面)應是如下所寫:
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 |
- (void)serviceNetWorkMessageListWith Success:(void(^)(id responseObject))successBlock//請求成功回撥 ReceiveFail:(void (^)(id responseObject))ReceiveFailBolck//請求失敗,伺服器返回錯誤資訊 Failure:(void(^)(NSError *error))failBlock//請求失敗,網路不通 { NSMutableDictionary *tempDic = [@{@"service_code":Message_Service_Code} mutableCopy]; [tempDic addEntriesFromDictionary:[self getPrivateParameters]]//所有介面的公共訪問引數; [tempDic addEntriesFromDictionary:@{"":""}//對應介面的對應引數]; //這裡舉個Archive做資料快取的讀取與儲存的例子,方便大家理解在某些場景下需要某個ViewModel作快取時,如何只對外提供一個資料介面,裡面作多級快取的原理,具體實現大家可以結合自身優化,比如資料庫的快取,比如在這個類單獨歸一個方法。 if ([[NSUserDefaults standardUserDefaults] objectForKey:Message_Service_Code]) { NSArray *priarr = [[NSUserDefaults standardUserDefaults] objectForKey:Message_Service_Code]; NSMutableArray *priMuarr = [NSMutableArray arrayWithCapacity:0]; for (NSData *data in priarr) { MessageInfoModel *message = [NSKeyedUnarchiver unarchiveObjectWithData:data];//解檔 [priMuarr addObject:message]; } successBlock(priMuarr);//返回快取資料 } [self serviceNetWorkWithUrlStr:[UrlGenerator queryNewMessageUrl] Params:tempDic Success:^(id responseObject) { if ([responseObject[@"code"] isEqualToString:@"0000"]) { NSArray *array = [MessageInfoModel mj_objectArrayWithKeyValuesArray:responseObject[@"data"]]; NSMutableArray *enArr = [NSMutableArray arrayWithCapacity:0]; for (MessageInfoModel *messageModel in array) { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:messageModel]; [enArr addObject:data];//歸檔 } [[NSUserDefaults standardUserDefaults] setObject:enArr forKey:Message_Service_Code]; successBlock(array);//返回網路請求資料 }else { ReceiveFailBolck(responseObject[@"msg"]); } } Failure:^(NSError *error) { failBlock(error); }]; } |
3、到底ViewModel放在哪裡合適?建立多少個ViewModel合適?
開門見山直說,個人認為只要涉及資料的View的模組,最理想的情況應該一個View模組一個ViewModel。
因為,只有這樣,在後期專案越來越龐大,維護的人員越來越多的時候,才能保證模組之間的絕對獨立。
(如果是一兩個人開發的中小型專案,也沒必要一個模組對一個ViewModel,本身功能不多,沒有必要。這時可以一個Controller對一個ViewModel,或者幾個同需求功能的Controller對一個ViewModel。專案死的,人是靈活的,具體情況具體分析。)
我舉兩個例子:
a.某個模組因為維護人員頻次多,需求改動頻繁,到最後需要大改某個模組的時候,因為之前的至上到下(資料到介面)的絕對獨立,可以完全抽出來,重新制作一個新的模組,重新替換進去;
b.某些模組需要資料快取,某些模組又需要網路狀態判斷(含大圖片展示的模組),完全可以針對不同情況針對這些相應模組對應的ViewModel資料工廠做相應的事情。
以上兩個例子,我想做過專案的,尤其是遇到頻繁更改需求的,應該是深有體會(哎,我本人就特別有體會/(ㄒoㄒ)/~~)。
借用一位大神的話結束Model篇:
“你必須得清楚你要做什麼,業務方希望要什麼。而不是為了架構而架構,也不是為了體驗新技術而改架構方案。以前是MVC,最近流行MVVM,如果過去的MVC是個好架構,沒什麼特別大的缺陷,就不要推倒然後搞成MVVM。”
後面幾天我在各位大神的建議下會不斷優化文章各個細節,歡迎多多關注,共同學習。幾天後會發出 view–業務與介面結合的模組開發 請多關注。