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


https://cloud.tencent.com/developer/article/1117009

一:為什么要用MVVM?


為什么要用MVVM?只是因為它不會讓我時常懵逼。

每次做完項目過后,都會被自己龐大的ViewController代碼嚇壞,不管是什么網絡請求、networking data process、跳轉交互邏輯統統往ViewController里面塞,就算是自己寫的代碼,也不敢直視。我不得不思考是不是MVC模式太過落后了,畢竟它叫做Massive View Controller,其實說MVC落后不太合理,說它太原生了比較合適。

MVC模式的歷史非常的久遠,它其實不過是對編程模式的一種模塊化,不管是MVVM、MVCS、還是聽起來就毛骨悚然的VIPER,都是對MVC標准的三個模塊的繼續划分,細分下去,使每個模塊的功能更加的獨立和單一,而最終目的都是為了提升代碼的規范程度,解耦,和降低維護成本。具體用什么模式需要根據項目的需求來決定,而這里,我簡單的說說自己對MVVM架構的理解和設計思想,淺談拙見。

二:MVVM模塊划分


傳統的MVC模式分為:Model、View、Controller。Model是數據模型,有胖瘦之分,View負責界面展示,而Controller就負責剩下的邏輯和業務,瞬間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模式的結合使用只能用一個詞形容————完美。

當然,有些開發者不太願意用這些東西,大概是因為他們覺得這破壞了代理、通知、監聽、block等的復雜邏輯觀感,但是我在這里大力推崇RAC,因為我的MVVM搭建思路里面會涉及大量的屬性綁定、事件傳遞,我可不想寫上一萬個協議來實現這些簡單的功能,運用RAC能大量簡化代碼,使邏輯更加的清晰。

接下來我將對我的MVVM架構實現思路做一個詳細的講解,在這之前,如果你沒有用過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<MineTopCollectionViewCellViewModel *> *dataSorceOfMineTopCollectionViewCell;
@property (nonatomic, strong) NSArray<MineDownCollectionViewCellViewModel *> *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、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<RACSubscriber> 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.headerBackgroundImage = [UIImage imageNamed:@"BG"];
        _mineHeaderViewModel.headerImageUrlStr = nil;
        [[[RACObserve([LoginBackInfoModel shareLoginBackInfoModel], headimg) distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) {
            if (x == nil) {
                _mineHeaderViewModel.headerImageUrlStr = nil;
            } else {
                _mineHeaderViewModel.headerImageUrlStr = x;
            }
        }];
        ......
    return _mineHeaderViewModel;
}
- (NSArray<MineTopCollectionViewCellViewModel *> *)dataSorceOfMineTopCollectionViewCell {
    if (!_dataSorceOfMineTopCollectionViewCell) {
        MineTopCollectionViewCellViewModel *model1 = [MineTopCollectionViewCellViewModel new];
        MineTopCollectionViewCellViewModel *model2 = [MineTopCollectionViewCellViewModel new];
        ......
        _dataSorceOfMineTopCollectionViewCell = @[model1, model2];
    }
    return _dataSorceOfMineTopCollectionViewCell;
}
- (NSArray<MineDownCollectionViewCellViewModel *> *)dataSorceOfMineDownCollectionViewCell {
    if (!_dataSorceOfMineDownCollectionViewCell) {
        ......
    }
    return _dataSorceOfMineDownCollectionViewCell;
}

@end

  

為了方便,我直接將以前寫的一些代碼貼上來了,不要被它的長度嚇着了,你完全可以忽略內部實現,只需要知道,這里不過是實現了RACCommand和RACSubject以及初始化子ViewModel。

是的,主ViewModel的主要工作基本上只有這三個。

關於屬性綁定的邏輯,我將在之后講到。

我們先來看看子ViweModel的觀感:

@interface MineTopCollectionViewCellViewModel : NSObject
@property (nonatomic, strong) UIImage *headerImage;
@property (nonatomic, copy) NSString *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的數據屬性展示在界面上。如果你會一些RAC,當然實現這些就是小菜一碟,但是如果你堅持蘋果原生的協議、通知,實現起來就會有一點麻煩(代碼量啊!!!)。

(2)繼承有復用機制的View(UICollectionviewCell等)

最值得注意的地方就是cell、item的復用機制問題了。

我們在自定義這些cell、item的時候,並不能綁定相應的ViewModel,因為它的復用原理,將會出現多個cell(item)的ViewModel一模一樣,在這里,我選擇了一個我自認為最好的方案來解決。

首先,在自定義的cell(item).h中聲明一個ViewModel屬性。

#import <UIKit/UIKit.h>
#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.headerImageView.image = viewModel.headerImage;
    self.headerLabel.text = viewModel.headerTitle;

}

  

ps:這里再次看到RAC()和RACObserve()這兩個宏,這是屬性綁定,如果你不懂,可以先不用管,在后面我會講解一下我的屬性綁定思路,包括不使用ReactiveCocoa達到同樣的效果(這完全是作死啊!!!)。

重寫setter的作用大家應該知道吧,就是在collection view的協議方法中寫到:

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];
    
    [AutoLoginAPIManager new];
    
    [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屬性會自動做出相應的處理變化。

這樣就可以解決相當多的需求了,比如:用戶信息展示界面->登錄界面->登錄成功->回到用戶信息展示界面->展示用戶信息

以往我們的做法通常是,用戶信息展示界面寫一個通知監聽->登錄成功發送通知->用戶信息展示界面刷新布局

當然,也可以用協議、block什么的,這么一看貌似並沒有多么復雜,但是一旦代碼量多了過后,你就知道什么叫懵逼了,而使用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+RAC雖然很強大,但是在有時候還是會增加代碼量,其實MVVM和Android里面的MVP模式有相當多的共同點,可以借鑒了解;至於MVCS沒有什么可講的,VIPER模式看起來比較厲害,想一想可能又是把哪個模塊細化了,猜測ViewModel?嘿嘿,其實我沒研究過VIPER,就不班門弄斧了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM