iOS開發之MVVM+RAC架構模式

在說MVVM之前,首先要了解我們最常用的MVC設計模式⬇️

1.MVC設計模式

蘋果官方將MVC設計模式作爲iOS APP的標準模式

斯坦福大學公開課上的這幅圖來說明,這可以說是最經典和最規範的MVC標準


MVC是最普遍認知的設計模式(高內聚,低耦合),MVC模式將頁面的邏輯分爲3塊:Model(模型數據業務)、View(UI展示業務)、Controller(協調者-控制器)

Model(模型):是應用程序中用於處理應用程序數據邏輯的部分。
    通常模型對象負責在數據庫中存取數據。

比如我們人類有一雙手,一雙眼睛,一個腦袋,沒有尾巴,這就是模型,Model定義了這個模塊的數據模型。
在代碼中體現爲數據管理者,Model負責對數據進行獲取及存放。
數據不可能憑空生成的,要麼是從服務器上面獲取到的數據,要麼是本地數據庫中的數據,
也有可能是用戶在UI上填寫的表單即將上傳到服務器上面存放,所以需要有數據來源。
既然Model是數據管理者,則自然由它來負責獲取數據。
Controller不需要關心Model是如何拿到數據的,只管調用就行了。
數據存放的地方是在Model,而使用數據的地方是在Controller,
所以Model應該提供接口供controller訪問其存放的數據(通常通過.h裏面的只讀屬性)

View(視圖):是應用程序中處理數據顯示的部分。
    通常視圖是依據模型數據創建的。

View,視圖,簡單來說,就是我們在界面上看見的一切。
它們有一部分是我們UI定死的,也就是不會根據數據來更新顯示的,
比如一些Logo圖片啊,這裏有個按鈕啊,那裏有個輸入框啊,一些顯示特定內容的label啊等等;
有一部分是會根據數據來顯示內容的,比如tableView來顯示好友列表啊,
這個tableView的顯示內容肯定是根據數據來顯示的。
我們使用MVC解決問題的時候,通常是解決這些根據數據來顯示內容的視圖。

Controller(控制器):是應用程序中處理用戶交互的部分。
    通常控制器負責從視圖讀取數據,控制用戶輸入,並向模型發送數據。

Controller是MVC中的數據和視圖的協調者,也就是在Controller裏面把Model的數據賦值給View來顯示
(或者是View接收用戶輸入的數據然後由Controller把這些數據傳給Model來保存到本地或者上傳到
服務器)。

controller出現的原因:我們所有的App都是界面和數據的交互,所以需要類來進行界面的繪製,於是出現了View,需要類來管理數據於是出現了Model。我們設計的View應該能顯示任意的內容比如頁面中顯示的文字應該是任意的而不只是某個特定Model的內容,所以我們不應該在View的實現中去寫和Model相關的任何代碼,如果這樣做了,那麼View的可擴展性就相當低了。而Model只是負責處理數據的,它根本不知道數據到時候會拿去幹啥,可能拿去作爲算法噼裏啪啦去了,可能拿去顯示給用戶了,它既然無法接收用戶的交互,它就不應該去管和視圖相關的任何信息,所以Model中不應該寫任何View相關代碼。然而我們的數據和界面應該同步,也就是一定要有個地方要把Model的數據賦值給View,而Model內部和View的內部都不可能去寫這樣的代碼,所以只能新創造一個類出來了,取名爲Controller

2.MVC缺點

上述對於MVC的描述是理想狀態下的MVC,Controller的作用應該是一個橋樑,建立起ModelView的連接,但實際開發中總是會出現Controller厚重,重量級控制器的問題,原因如下

  • 繁重的UI
  • 囉嗦的業務邏輯
  • 各種代理
  • 遺失的網絡邏輯(無立足之地)
  • 較差的可測試性

在iOS開發中,UIKIt框架是將控制器Controller與View進行綁定了的,每個控制器都有View對象,代碼添加UI子控件細節或者在xib與storyboard中子視圖可以直接與controller進行關聯,都會導致控制器中難以避免很多本該View去負責的UI子控件細節處理放在了控制器Controller裏面;而在Controller裏面本身要處理的請求、控制器生命週期函數要處理的事情比較多的情況下,控制器就變得很臃腫。實際上這個設計模式在iOS中爲:M-VC

因此,M-VC 可能是對 iOS 開發中的 MVC模式更爲準確的解讀,同時更也準確地描述了我們日常開發可能已經編寫的 MVC 代碼,但它並沒有做太多事情來解決 iOS 應用中日益增長的重量級視圖控制器的問題。

舉個例子: cell傳值,就需要在VC裏解析完數據在將model傳給cell的view,增加了model和view之間的耦合,就變成了M-VC

這裏還要特殊說一下那無處安放網絡請求

蘋果使用的MVC的定義是這麼說的:所有的對象都可以被歸類爲一個model,一個view,或是一個controller。就這些。那麼把網絡代碼放哪裏?和一個API通信的代碼應該放在哪兒?
你可能試着把它放在model對象裏,但是也會很棘手,因爲網絡調用應該使用異步,這樣如果一個網絡請求比持有它的model生命週期更長,事情將變的複雜。顯然也不應該把網絡代碼放在view裏,因此只剩下controller了。這同樣是個壞主意,因爲這加劇了厚重View Controller的問題。
那麼應該放在那裏呢?顯然MVC的3大組件根本沒有適合放這些代碼的地方。

3.MVVM設計模式

MVVM的誕生

就像我們之前分析MVC是如何合理分配工作的一樣,我們需要數據所以有了M,我們需要界面所以有了V,而我們需要找一個地方把M賦值給V來顯示,所以有了C,然而我們忽略了一個很重要的操作:數據解析。在MVC出生的年代,手機APP的數據往往都比較簡單,沒有現在那麼複雜,所以那時的數據解析很可能一步就解決了,所以既然有這樣一個問題要處理,而面向對象的思想就是用類和對象來解決問題,顯然V和M早就被定義死了,它們都不應該處理“解析數據”的問題,理所應當的,“解析數據”這個問題就交給C來完成了。而現在的手機App功能越來越複雜,數據結構也越來越複雜,所以數據解析也就沒那麼簡單了。如果我們繼續按照MVC的設計思路,將數據解析的部分放到了Controller裏面,那麼Controller就將變得相當臃腫。還有相當重要的一點:Controller被設計出來並不是處理數據解析的。1、管理自己的生命週期;2、處理Controller之間的跳轉;3、實現Controller容器。這裏面根本沒有“數據解析”這一項,所以顯然,數據解析也不應該由Controller來完成。那麼我們的MVC中,M、V、C都不應該處理數據解析,那麼由誰來呢?這個問題實際上在面向對象的時候相當好回答:既然目前沒有類能夠處理這個問題,那麼就創建一個新的類出來解決不就好了?所以我們聰明的開發者們就專門爲數據解析創建出了一個新的類:ViewModel。這就是MVVM的誕生。

MVVVM解決的問題,你只需要記住兩點:1、Controller的存在感被完全的降低了;2、VM的出現就是Controller存在感降低的原因。

Controller存在感降低的原因

在MVVM中,Controller不再像MVC那樣直接持有Model了。想象Controller是一個Boss,數據是一堆文件(Model),如果現在是MVC,那麼數據解析(比如整理文件)需要由Boss親自完成,然而實際上Boss需要的僅僅是整理好的文件而不是那一堆亂七八糟的整理前的文件。所以Boss招聘了一個祕書,現在Boss就不再需要管理原始數據(整理之前的文件)了,他只需要去找祕書:你幫我把文件整理好後給我。那麼這個祕書就首先去拿到文件(原始數據),然後進行整理(數據解析),接下來把整理的結果給Boss。所以祕書就是VM了,並且Controller(Boss)現在只需要直接持有VM而不需要再持有M了。如果再進一步理解C、VM、M之間的關係:因爲Controller只需要數據解析的結果而不關心過程,所以就相當於VM把“如何解析Model”給封裝起來了,C甚至根本就不需要知道M的存在就能把工作做好,前提它需要持有一個VM。那麼我們MVVM中的持有關係就是:C持有VM,VM持有M。這裏有一個比較爭議的地方:C該不該持有M。我的答案是不該。爲什麼呢,因爲C持有M沒有任何意義。就算C直接拿到了M的數據,它還是要去讓VM進行數據解析,而數據解析就需要M,那麼直接讓VM持有M而C直接持有VM就足夠了。最後再分享一個我在實現MVVM中的一個技巧,也談不上是技巧吧,算是一種必要的思想:一旦在實現Controller的過程中遇到任何跟Model(或者數據)相關的問題,就找VM要答案。這個思想待會我們會在實現代碼的時候用到。

MVVM雙向綁定的實現

使用block和監聽實現該功能

ViewController核心代碼

@property (nonatomic, strong) NSMutableArray *dataArray;
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) MVVMViewModel *vm;

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.tableView];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    
    self.vm = [[MVVMViewModel alloc] init];
    __weak typeof(self) weakSelf = self;
    //model -> UI -> 代碼塊
    [self.vm initWithBlock:^(id data) {
        //獲取到更新後的數據,刷新UI
        NSArray *array = data;
        [weakSelf.dataArray removeAllObjects];
        [weakSelf.dataArray addObjectsFromArray:array];
        
        MVVMView *view = [[MVVMView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, (array.count + 1)/4*50)];
        [view headViewWithData:array];
        weakSelf.tableView.tableHeaderView = view;
        [weakSelf.tableView reloadData];
    } WithErrorBlock:^(id errorCode) {
        
    }];
    
    [self.vm reloadData];
}
#pragma mark - tableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    self.vm.contenKey = self.dataArray[indexPath.row];   
    //這裏通過修改MVVMViewModel的contenKey來更新數據(觸發通知方法),更新好的數據在通過block回調到VC中,完成了MVVM的雙向綁定
}

ViewModel核心代碼

//.h代碼
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "ViewModelClass.h"   //父類爲封裝好方便調用block的基類,具體可參照demo
@interface MVVMViewModel : ViewModelClass
@property (nonatomic, copy) NSString *contenKey;  //通過KVO監聽該值,當該值發生改變後更新數據,回調VC

- (void)reloadData;  
@end
//.m代碼
#import "MVVMViewModel.h"
@implementation MVVMViewModel

- (instancetype)init{
    if (self == [super init]) {
        [self addObserver:self forKeyPath:@"contenKey" options:NSKeyValueObservingOptionNew context:nil];
    }
    return self;
}

- (void)reloadData{
    //數據一般爲網絡獲取,獲取後進行回調,VC刷新頁面
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSArray *array = @[@"轉賬",@"信用卡",@"充值中心",@"螞蟻借唄",@"電影票",@"滴滴出行",@"城市服務",@"螞蟻森林"];
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.successBlock) {
                //返回block
                self.successBlock(array);
            }
        });
    });
}

#pragma mark - KVO回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);    //通過監聽得知數據變化
    //處理數據
    NSArray *array = @[@"轉賬",@"信用卡",@"充值中心",@"螞蟻借唄",@"電影票",@"滴滴出行",@"城市服務",@"螞蟻森林"];
    NSMutableArray *mArray = [NSMutableArray arrayWithArray:array];
    [mArray removeObject:change[NSKeyValueChangeNewKey]];
    //雙向綁定通知VC更新變化後的數據
    if (self.successBlock) {
        self.successBlock(mArray);
    }
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"contenKey"];  //移除通知
}

@end

實際開發中,我們可以搭配RAC使用MVVM的架構模式,代碼更簡潔、易懂

4.ReactiveCocoa簡介

ReactiveCocoa是響應式編程(FRP)在iOS和OS中的一個實現框架,它的開源地址爲:https://github.com/ReactiveCocoa/ReactiveCocoa

優點

RAC雖然最大的優點是提供了一個單一的、統一的方法去處理異步的行爲,包括delegate方法、blocks回調、target-action機制、notificationsKVO

詳細來說,在iOS開發過程中,當某些事件響應的時候,需要處理某些業務邏輯,這些事件都用不同的方式來處理。
比如按鈕的點擊使用action,ScrollView滾動使用delegate,屬性值改變使用KVO等系統提供的方式。
其實這些事件,都可以通過RAC處理
ReactiveCocoa爲事件提供了很多處理方法,而且利用RAC處理事件很方便,可以把要處理的事情,和監聽的事情的代碼放在一起,這樣非常方便我們管理,就不需要跳到對應的方法裏。非常符合我們開發中高聚合,低耦合的思想。

集成RAC

通過pods集成,集成步驟可以參考這兩篇文章
ios 通過CocoaPods安裝第三方庫
Xcode如何集成Pod教程
ps:該三方庫支持多個平臺iOS、OS、Swift,iOS集成pod 'ReactiveObjC', '~> 3.1.0'

RAC常用語法
  • UITextField
    @weakify(self);
    [[self.testTextFileld rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
    @strongify(self);
        NSLog(@"%@",x);
        self.testTextFileld.text = @"Hello";
    }];

監聽了輸入框內所有的變化,包括準備編輯,和退出編輯。再也不用寫delegate了,編碼起來方便快捷!!!

  • UIButton
    [[self.btn rac_signalForControlEvents:(UIControlEventTouchUpInside)] subscribeNext:^(__kindof UIControl * _Nullable x) {
        NSLog(@"%@",[x class]);
    }];

平常寫按鈕的觸發事件都要新建一個方法去實現,現在不用了,直接在你的按鈕下面寫實現的代碼。實例化和觸發事件寫在一起,查閱代碼和維護代碼更加直觀!!!

  • NSNotificationCenter
    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
        NSLog(@"%@",x);
    }];

還能監聽通知的各種事件,上面就是監聽了APP退到後臺的事件。最重要的一點就是不需要移除通知,比通知用起來更爽,無後顧之憂!!!

MVVM+RAC代碼

只需要把上述代碼的init方法修改爲RAC代碼即可

- (instancetype)init{
    if (self == [super init]) {
        //訂閱信號(熱信號!!)    監聽contenKey值的變化
        [RACObserve(self, contenKey) subscribeNext:^(id  _Nullable x) {
            NSArray *array = @[@"轉賬",@"信用卡",@"充值中心",@"螞蟻借唄",@"電影票",@"滴滴出行",@"城市服務",@"螞蟻森林"];
            NSMutableArray *mArray = [NSMutableArray arrayWithArray:array];
            [mArray removeObject:x];
            if (self.successBlock) {
                self.successBlock(mArray);
            }
        }];
    }
    return self;
}

上述代碼已經實現了MVVM+RAC的開發,那麼我們項目中的MVC架構模式應該如何去優化呢?

5.如何對 ViewController 瘦身?

  • 將網絡請求抽象到單獨的類中
  • 將界面的拼裝抽象到專門的類中
  • 創建構造類似 ViewModel 的工廠類,參見 工廠模式。另外,也可以專門將數據存取都抽將到一個 Service 層,由這層來提供 ViewModel 的獲取。

6.MVVM 存在的問題

  • 數據綁定使得 Bug 很難被調試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數據綁定使得一個位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那麼容易了。
  • 對於過大的項目,數據綁定需要花費更多的內存。
  • 存在一定的學習成本和引入更多的三方庫(RAC等等),代碼邏輯更復雜

7.總結

  • MVC的設計模式也並非是病入膏肓,無藥可救的架構,最起碼目前MVC設計模式仍舊是iOS開發的主流框架,存在即合理。針對文章所述的弊端,我們依舊有許多可行的方法去避免和解決,從而打造一個輕量級的ViewController

  • MVVMMVC的升級版,完全兼容當前的MVC架構,MVVM雖然促進了UI 代碼與業務邏輯的分離,一定程度上減輕了ViewController的臃腫度,但是ViewViewModel之間的數據綁定使得MVVM變得複雜和難用了,如果我們不能更好的駕馭兩者之間的數據綁定,同樣會造成Controller 代碼過於複雜,代碼邏輯不易維護的問題。

  • 一個輕量級的ViewController是基於MVCMVVM模式進行代碼職責的分離而打造的。MVCMVVM有優點也有缺點,但缺點在他們所帶來的好處面前時不值一提的。他們的低耦合性,封裝性,可測試性,可維護性和多人協作便利大大提高了開法效率。

  • 同時,我們需要保持的是一個擁抱變化的心,以及理性分析的態度。在新技術的面前,不盲從,也不守舊,一切的決策都應該建立在認真分析的基礎上,這樣才能應對技術的變化。

  • 在選擇架構的方向上,請記住這句話:沒有最好的架構,只有最適合的

Demo下載:
https://github.com/gaoyuGood/MVVM-RAC

參考文獻:
iOS 關於MVC和MVVM設計模式的那些事
mvc和mvvm的區別
淺談 MVC、MVP 和 MVVM 架構模式
被誤解的 MVC 和被神化的 MVVM
iOS開發之RAC(一)初級篇
最快讓你上手ReactiveCocoa之基礎篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章