作者:indulge_in
連結:https://www.jianshu.com/p/4921825f3bbe
一:為什麼要用MVVM?
為什麼要用MVVM?只是因為它不會讓我時常懵逼。
每次做完專案過後,都會被自己龐大的 ViewController 程式碼嚇壞,不管是什麼網路請求、網路資料處理、跳轉互動邏輯統統往 ViewController 裡面塞,就算是自己寫的程式碼,也不敢直視。我不得不思考是不是MVC樣式太過落後了,畢竟它叫做 Massive View Controller,其實說 MVC 落後不太合理,說它太原始了比較合適。
MVC 樣式的歷史非常的久遠,它其實不過是對工程程式碼的一種模組化,不管是 MVVM、MVCS、還是聽起來就毛骨悚然的 VIPER,都是對 MVC 標準的三個模組的繼續劃分,細分下去,使每個模組的功能更加的獨立和單一,而最終目的都是為了提升程式碼的規範程度,解耦,和降低維護成本。具體用什麼樣式需要根據專案的需求來決定,而這裡筆者就 MVVM 架構和設計,淺談拙見。
二:MVVM模組劃分
傳統的MVC樣式分為:Model、View、Controller。Model 是資料模型,有胖瘦之分,View 負責介面展示,而 Controller 就負責剩下的邏輯和業務。
MVVM 樣式多了一個 ViewModel,它的作用是為 Controller 減負,將Controller裡面的邏輯(主要是弱業務邏輯)轉移到自身,其實它涉及到的工作不止是這些,還包括頁面展示資料的處理等。(後序章節會有具體講解)
我的設計是這樣的:
-
一個 View 對應一個 ViewModel,View 介面元素屬性與 ViewModel 處理後的資料屬性系結
-
Model 只是在有網路資料的時候需要建立,它的作用只是一個資料的中專站,也就是一個極為簡介的瘦 Model
-
這裡弱化了 Model 的作用,而將對網路資料的處理的邏輯放在 ViewModel 中,也就是說,只有在有網路資料展示的 View 的 ViewModel 中,才會看見 Model 的影子,而處理過後的資料,將變成 ViewModel 的屬性,註意一點,這些屬性一定要儘量“直觀”,比如能寫成UIImage 就不要寫成 URL
-
ViewModel 和 Model 可以視情況看是否需要屬性系結
-
Controller 的作用就是將主View透過與之對應的 ViewModel 初始化,然後新增到 self.view,然後就是監聽跳轉邏輯觸發等少部分業務邏輯,當然,ViewController 的路由跳轉還可以解放出來。
-
註意:這裡面提到的系結,其實就是對屬性的監聽,當屬性變化時,監聽者做一些邏輯處理,由此涉及到一個框架————RAC
三:ReactiveCocoa
RAC是一個強大的工具,它和MVVM樣式的結合使用只能用一個詞形容 ——— 完美。
當然,有些開發者不太願意用這些東西,大概是因為他們覺得這破壞了代理、通知、監聽、閉包等的邏輯觀感。但是筆者 MVVM 搭建思路裡面會涉及大量的屬性系結、事件傳遞,運用 RAC 能大量簡化程式碼,使邏輯更加的清晰。
在這之前,如果你沒有用過RAC,請先移步:
大致的瞭解一下RAC過後,便可以往下(^)
四:MVVM模組具體實現
這是要實現的介面:
1、Model
這裡弱化了 Model,只是做為資料模型使用。只有在 View 需要顯示網路資料的時候,對應的 ViewModel 裡面才有 Model 的相關處理。
2、ViewModel
在實際開發當中,一個 View 對應一個 ViewModel,主 View 對應並且系結一個主 ViewModel。
主 ViewModel 承擔了網路請求、點選事件協議、初始化子 ViewModel 並且給子 ViewModel 的屬性賦初值;網路請求成功傳回資料過後,主 ViewModel 還需要給子 ViewModel 的屬性賦予新的值。
主 ViewModel 的觀感是這樣的:
@interface MineViewModel : NSObject
//viewModel
@property (nonatomic, strong) MineHeaderViewModel *mineHeaderViewModel;
@property (nonatomic, strong) NSArray *dataSorceOfMineTopCollectionViewCell;
@property (nonatomic, strong) NSArray *dataSorceOfMineDownCollectionViewCell;
//RACCommand
@property (nonatomic, strong) RACCommand *autoLoginCommand;
//RACSubject
@property (nonatomic, strong) RACSubject *pushSubject;
@end
其中,RACCommand 是放網路請求的地方,RACSubject 相當於協議,這裡用於點選事件的代理,而 ViewModel 下麵的一個 ViewModel 屬性和三個裝有 ViewModel 的陣列需要著重說一下。
在iOS開發中,我們通常會自定義 View,而自定義的 View 有可能是繼承自 UICollectionviewCell(UITableViewCell、UITableViewHeaderFooterView 等),當我們自定義一個 View 的時候,這個 View 不需要復用且只有一個,我們就在主 ViewModel 宣告一個子 ViewModel 屬性,當我們自定義一個需要復用的 cell、item、essay-headerView 等的時候,我們就在主 ViewModel 中宣告陣列屬性,用於儲存復用的 cell、item 的 ViewModel,中心思想仍然是一個 View 對應一個 ViewModel。
在.m檔案中,對這些屬性做懶載入處理,並且將 RACCommand 和 RACSubject 配置好,方便之後在需要的時候觸發以及呼叫,程式碼如下:
@implementation MineViewModel
- (instancetype)init
{
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
[self.autoLoginCommand.executionSignals.switchToLatest subscribeNext:^(id responds) {
//處理網路請求資料
......
}];
}
#pragma mark *** getter ***
- (RACSubject *)pushSubject {
if (!_pushSubject) {
_pushSubject = [RACSubject subject];
}
return _pushSubject;
}
- (RACCommand *)autoLoginCommand {
if (!_autoLoginCommand) {
_autoLoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id subscriber) {
NSDictionary *paramDic = @{......};
[Network start:paramDic success:^(id datas) {
[subscriber sendNext:datas];
[subscriber sendCompleted];
} failure:^(NSString *errorMsg) {
[subscriber sendNext:errorMsg];
[subscriber sendCompleted];
}];
return nil;
}];
}];
}
return _autoLoginCommand;
}
- (MineHeaderViewModel *)mineHeaderViewModel {
if (!_mineHeaderViewModel) {
_mineHeaderViewModel = [MineHeaderViewModel new];
_mineHeaderViewModel.essay-headerBackgroundImage = [UIImage imageNamed:@"BG"];
_mineHeaderViewModel.essay-headerImageUrlStr = nil;
[[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], headimg) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
if (x == nil) {
_mineHeaderViewModel.essay-headerImageUrlStr = nil;
} else {
_mineHeaderViewModel.essay-headerImageUrlStr = x;
}
}];
......
return _mineHeaderViewModel;
}
- (NSArray *)dataSorceOfMineTopCollectionViewCell {
if (!_dataSorceOfMineTopCollectionViewCell) {
MineTopCollectionViewCellViewModel *model1 = [MineTopCollectionViewCellViewModel new];
MineTopCollectionViewCellViewModel *model2 = [MineTopCollectionViewCellViewModel new];
......
_dataSorceOfMineTopCollectionViewCell = @[model1, model2];
}
return _dataSorceOfMineTopCollectionViewCell;
}
- (NSArray *)dataSorceOfMineDownCollectionViewCell {
if (!_dataSorceOfMineDownCollectionViewCell) {
......
}
return _dataSorceOfMineDownCollectionViewCell;
}
@end
為了方便,我直接將以前寫的一些程式碼貼上來了,不要被它的長度嚇著了,你完全可以忽略內部實現,只需要知道,這裡不過是實現了 RACCommand 和 RACSubject 以及初始化子 ViewModel。
是的,主 ViewModel 的主要工作基本上只有這三個。
關於屬性系結的邏輯,我將在之後講到。
我們先來看看子 ViweModel 的觀感:
@interface MineTopCollectionViewCellViewModel : NSObject
@property (nonatomic, strong) UIImage *essay-headerImage;
@property (nonatomic, copy) NSString *essay-headerTitle;
@property (nonatomic, copy) NSString *content;
@end
我沒有貼.m裡面的程式碼,因為裡面沒有程式碼(嘿嘿)。
接下來說說,為什麼我設計的子 ViewModel 只有幾個單一的屬性,而主 ViewModel 卻有如此多的邏輯。
首先,我們來看一看 ViewModel 的概念,Model 是模型,所以 ViewModel 就是檢視的模型。而在傳統的 MVC 中,瘦 Model 叫做資料模型,其實瘦 Model 叫做 DataModel 更為合適;而胖 Model 只是將網路請求的邏輯、網路資料處理的邏輯寫在了裡面,方便於 View 更加便捷的展示資料,所以,胖 Model 的功能和 ViewModel 大同小異,筆者把它叫做“少根筋的 ViewModel”。
這麼一想,我們似乎應該將網路資料處理的邏輯放在子 ViewModel 中,來為主 ViewModel 減負。
筆者也想這麼做。
但是有個問題,舉個簡單的例子,比如這個需求:
一般的思路是自定義一個 CollectionviewCell 和一個 ViewModel,因為它們的佈局是一樣的,我們需要在主 ViewModel 中宣告一個陣列屬性,然後放入兩個 ViewModel,分別對應兩個 Cell。
image 和 title 這種靜態資料我們可以在主ViewModel中為這兩個子ViewModel賦值,而下方的具體額度和數量來自網路,網路請求下來的資料通常是:
{
balance:"100"
redPacket:"3"
}
我們需要把”100“轉化為”100元“,”3“轉化為”3個“。
這個網路資料處理邏輯按正常的邏輯來說是應該放在 ViewModel 中的,但是有個問題,我們這個 collectionviewcell 是復用的,它的 ViewModel 也是同一個,而處理的資料是兩個不同的欄位,我們如何區分?而且不要忘了,網路請求成功獲得的資料是在主 ViewModel 中的,還涉及到傳值。再按照這個思路去實現必然更為複雜,所以我乾脆一刀切,不管是靜態資料還是網路資料的處理,通通放在主 ViewModel 中。
這樣做雖然讓主 ViewModel 任務繁重,子 ViewModel 過於輕量,但是帶來的好處卻很多,一一列舉:
-
在主 ViewModel 的懶載入中,實現對子 ViewModel 的初始化和賦予初值,在RACCommand 中網路請求成功過後,主 ViewModel 需要再次給子 ViewModel 賦值。賦值條理清晰,兩個模組。
-
子 ViewModel 只放其對應的 View 需要的資料屬性,作用相當於 Model,但是比 Model 更加靈活,因為如果該 View 內部有著一些點選事件等,我們同樣可以在子 ViewModel 中新增RACSubject(或者協議)等,子 ViewModel 的靈活性很高。
-
不管是靜態資料還是網路資料統一處理,所有子 ViewModel 的初始化和屬性賦值放在一塊兒,所有網路請求放在一塊兒,所有 RACSubject 放在一塊兒,結構更加清晰,維護方便。
3、View
之前講到,ViewModel 和 Model 互動的唯一場景是有網路請求資料需要展示的情況,而 View 和 ViewModel 卻是一一對應,綁不繫結需要視情況而定。下麵詳細介紹。
自定義View這裡分兩種情況,分別處理:
(1)非繼承有復用機制的 View(不是繼承 UICollectionviewCell 等)
這裡以介面的主 View 為例
.h
- (instancetype)initWithViewModel:(MineViewModel *)viewModel;
該 View 需要和 ViewModel 系結,實現相應的邏輯和觸發事件,並且保證 ViewModel 的唯一性。
.m
這裡就不貼程式碼了,反正 View 與 ViewModel 的互動無非就是觸髮網路請求、觸發點選事件、將 ViewModel 的資料屬性展示在介面上。
(2)繼承有復用機制的 View(UICollectionviewCell 等)
最值得註意的地方就是 cell、item 的復用機制問題了。
我們在自定義這些 cell、item 的時候,並不能系結相應的 ViewModel,因為它的復用原理,將會出現多個 cell(item)的 ViewModel 一模一樣,在這裡,筆者使用瞭如下的解決方案:
首先,在自定義的 cell(item).h 中宣告一個 ViewModel 屬性。
#import
#import "MineTopCollectionViewCellViewModel.h"
@interface MineTopCollectionViewCell : UICollectionViewCell
@property (nonatomic, strong) MineTopCollectionViewCellViewModel *viewModel;
@end
然後,在該屬性的setter方法中給該cell的介面元素賦值:
#pragma mark *** setter ***
- (void)setViewModel:(MineTopCollectionViewCellViewModel *)viewModel {
if (!viewModel) return;
_viewModel = viewModel;
RAC(self, contentLabel.text) = [[RACObserve(viewModel, content) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal];
self.essay-headerImageView.image = viewModel.essay-headerImage;
self.essay-headerLabel.text = viewModel.essay-headerTitle;
}
ps:這裡再次看到RAC()和RACObserve()這兩個宏,這是屬性系結,如果你不懂,可以先不用管,在後面我會講解一下我的屬性系結思路,包括不使用 ReactiveCocoa 達到同樣的效果。
重寫setter的作用大家應該知道吧,就是在 collectionView 的協議方法中寫到:
cell.viewModel = self.viewModel.collectionCellViewModel;
的時候,能夠執行到該setter方法中,改變該cell的佈局。
好吧,這就是精髓,廢話不說了。
想了一下,還是貼上主 View 的 .m 程式碼吧:
@interface MineView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) MineViewModel *viewModel;
@end
@implementation MineView
- (instancetype)initWithViewModel:(MineViewModel *)viewModel
{
self = [super init];
if (self) {
self.backgroundColor = [UIColor colorWithRed:243/255.0 green:244/255.0 blue:245/255.0 alpha:1];
self.viewModel = viewModel;
[self addSubview:self.collectionView];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
[self bindViewModel];
}
return self;
}
- (void)updateConstraints {
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
[super updateConstraints];
}
- (void)bindViewModel {
[self.viewModel.autoLoginCommand execute:nil];
}
#pragma mark *** UICollectionViewDataSource ***
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 3;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (section == 1) return self.viewModel.dataSorceOfMineTopCollectionViewCell.count;
......
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 1) {
MineTopCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([MineTopCollectionViewCell class])] forIndexPath:indexPath];
cell.viewModel = self.viewModel.dataSorceOfMineTopCollectionViewCell[indexPath.row];
return cell;
}
......
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
......
}
#pragma mark *** UICollectionViewDelegate ***
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self.viewModel.pushSubject sendNext:nil];
}
#pragma mark *** UICollectionViewDelegateFlowLayout ***
......
#pragma mark *** Getter ***
- (UICollectionView *)collectionView {
if (!_collectionView) {
......
}
return _collectionView;
}
- (MineViewModel *)viewModel {
if (!_viewModel) {
_viewModel = [[MineViewModel alloc] init];
}
return _viewModel;
}
@end
4、Controller
這傢伙已經解放了。
@interface MineViewController ()
@property (nonatomic, strong) MineView *mineView;
@property (nonatomic, strong) MineViewModel *mineViewModel;
@end
@implementation MineViewController
#pragma mark *** life cycle ***
- (void)viewDidLoad {
[super viewDidLoad];
self.hidesBottomBarWhenPushed = YES;
[self.view addSubview:self.mineView];
[self bindViewModel];
}
- (void)updateViewConstraints {
[self.mineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
[super updateViewConstraints];
}
- (void)bindViewModel {
@weakify(self);
[[self.mineViewModel.pushSubject takeUntil:self.rac_willDeallocSignal] subscribeNext:^(NSString *x) {
@strongify(self);
[self.navigationController pushViewController:[LoginViewController new] animated:YES];
}];
}
#pragma mark *** getter ***
- (MineView *)mineView {
if (!_mineView) {
_mineView = [[MineView alloc] initWithViewModel:self.mineViewModel];
}
return _mineView;
}
- (MineViewModel *)mineViewModel {
if (!_mineViewModel) {
_mineViewModel = [[MineViewModel alloc] init];
}
return _mineViewModel;
}
@end
是不是非常清爽,清爽得甚至懷疑它的存在感了(_)。
五:附加講述
1、系結思想
我想,懂一些 RAC 的人都知道屬性系結吧,RAC(,)和 RACObserve(,),這是最常用的,它的作用是將 A 類的 a 屬性系結到 B 類的 b 屬性上,當 A 類的 a 屬性發生變化時,B 類的 b 屬性會自動做出相應的處理變化。
這樣就可以解決相當多的需求了,比如:使用者資訊展示介面->登入介面->登入成功->回到使用者資訊展示介面->展示使用者資訊
以往我們的做法通常是,使用者資訊展示介面寫一個通知監聽->登入成功傳送通知->使用者資訊展示介面掃清佈局
當然,也可以用協議、閉包什麼的。而使用 RAC 的屬性系結、屬性聯合等一系列方法,將會有事半功倍的效果,充分的降低了程式碼的耦合度,降低維護成本,思路更清晰。
在上面這個需求中,需要這樣做:
將使用者資訊展示 View 的屬性,比如 self.name,self.phone 等與對應的 ViewModel 中的資料系結。在主 ViewModel 中,為該子 ViewModel 初始化並賦值,使用者資訊展示 View 的內容就是這個初始值。當主 ViewModel 網路請求成功過後,再一次給該子 ViewModel 賦值,使用者資訊展示介面就能展示相應的資料了。
而且,我們還可以做得更好,就像我以上的程式碼裡面做的),將 View 的展示內容與 ViewModel 的屬性系結,將 ViewModel 的屬性與 Model 的屬性系結,看個圖吧:
只要 Model 屬性一變,傳遞到 View 使介面元素變化,全自動無新增。有了這個東西過後,以後 reloadData 這個方法可能見得就比較少了。
2、整體邏輯梳理
1、進入 ViewController,懶載入初始化主 View(呼叫-initWithViewMdoel方法,保證主 ViewModel 唯一性),懶載入初始化主ViewModel。
2、進入主 ViewModel,初始化配置網路請求、點選邏輯、初始化各個子 ViewModel。
3、進入主 View,透過主 ViewModel 初始化,呼叫 ViewModel 中的對應邏輯和對應子ViewModel 展示資料。
4、ViewController 與 ViewModel 的互動主要是跳轉邏輯等。
3、建立自己的架構
其實在任何專案中,如果某一個模組程式碼量太大,我們完全可以自己進行程式碼分離,只要遵循一定的規則(當然這是自己定義的規則),最終的目的都是讓功能和業務細化,分類。
這相當於在沙灘上抓一把沙,最開始我們將石頭和沙子分開,但是後來,發現沙子也有大有小,於是我們又按照沙子的大小分成兩部分,再後來發現沙子顏色太多,我們又把不同顏色的沙子分開……
在 MVVM 樣式中,完全可以把 ViewModel 的網路請求邏輯提出來,叫做 NetworkingCenter;還可以把 ViewModel 中的點選等各種監聽事件提出來,叫做 ActionCenter;還可以把介面展示的 View 的各種配置(比如在 tableView 協議方法中的寫的資料)提出來,叫做 UserInterfaceConfigDataCenter;如果專案中需要處理的網路請求資料很多,我們可以將資料處理邏輯提出來,叫做 DataPrecessCenter ……
記住一句話:萬變不離其宗。
六:結語
移動端的架構一直都是千變萬化,沒有萬能的架構,只有萬能的程式員,根據產品的需求選擇相應的架構才是正確的做法。MVC 固然古老,但是在小型專案卻依然實用;MVVM 雖然很強大,但是在有時候還是會增加程式碼量;VIPER 看似高大上,實際上應用場景比較少。在實際開發中,不拘泥於某種架構或者將它們結合起來用才是正確的做法。
●編號388,輸入編號直達本文
●輸入m獲取文章目錄
程式員求職面試
更多推薦《25個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。