歡迎光臨
每天分享高質量文章

iOS架構:MVVM設計樣式+RAC響應式程式設計

作者: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 (nonatomicstrong) MineHeaderViewModel *mineHeaderViewModel;
@property (nonatomicstrongNSArray *dataSorceOfMineTopCollectionViewCell;
@property (nonatomicstrongNSArray *dataSorceOfMineDownCollectionViewCell;

//RACCommand
@property (nonatomicstrong) RACCommand *autoLoginCommand;

//RACSubject
@property (nonatomicstrong) 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 (nonatomicstrongUIImage *essay-headerImage;
@property (nonatomiccopyNSString *essay-headerTitle;
@property (nonatomiccopyNSString *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 (nonatomicstrong) 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 () <UICollectionViewDelegateUICollectionViewDataSourceUICollectionViewDelegateFlowLayout>
@property (nonatomicstrongUICollectionView *collectionView;
@property (nonatomicstrong) 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 == 1return 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 (nonatomicstrong) MineView *mineView;
@property (nonatomicstrong) 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、資料庫、運維等。

    贊(0)

    分享創造快樂