通俗易懂圖解MVVM和RAC雙向繫結介紹(附Demo)

Deft_MKJing宓珂璟發表於2017-03-07

前言

一個前輩的MVVM介紹
其實MVVM就是MVC的進化版本,相對於臃腫的Controller,程式碼越來越多之後,有一部分人就用了新的設計模式,其實看久了也沒什麼,通俗點講,其實就是把之前Controller裡面的程式碼邏輯全部移植到了ViewModel裡面,相對於以前而言,控制器也被歸屬於View一類,那麼他和View一樣都會有自己的ViewModel去處理邏輯,而且ViewModel必然擁有Model,這樣的關係使得控制器程式碼會減少很多很多,處理起來又多了一個類,本身設計模式裡面有代理,通知,KVO等,不同業務對應不同的設計模式,個人理解為了減少控制器的程式碼,引進了新的類,那麼類的互動就變得更麻煩了,因此RAC出現了,他幫我們直接管理了蘋果的那一套資料處理設計模式,統一用它的”訊號流”來進行,誰用誰知道啊。。。。。。

雙向繫結
1.Model—->View 這種流向很簡單,你請求資料之後,通過Block的回撥,最終更新UI
2.View—–>Model 反向繫結也一樣,View觸發事件,更新對面ViewModel裡面繫結的資料來源,例如登入註冊的Textfield,你輸入和刪除的時候,你的Model欄位會對應更新,當你提交的時候,讀取ViewModel的欄位,就是已經更新的最新資料。這是一種方式,我個人感覺如下圖的另一種更容易理解,比如你選擇某個cell或者點讚的時候,View事件觸發,更新繫結的ViewModel欄位,擁有ViewModel的控制器,用RACObserve來進行該欄位開關的讀取,如果監聽到YES,就重新整理對應的頁面UI

簡單看下自己理解的MVVM
這裡寫圖片描述

效果圖
這裡寫圖片描述


圖片和文字閃爍的效果傳送門
閃爍效果

RAC的第一種流程介紹—>RACSignal

網上很多基本的介紹,這裡主要講一下流程
1.如果用RACSignal來建立訊號(內部Block有傳送訊號以及取消訊號的回撥,為什麼是3和4呢,原因在後面)

// 1.建立訊號
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3.傳送訊號
[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];
// 4.取消訊號,如果訊號想要被取消,就必須返回一個RACDisposable
// 當訊號被手動或者自動取消訂閱之後會回撥到這裡,進行一些資源的釋放
return [RACDisposable disposableWithBlock:^{
NSLog(@"取消訂閱");
}];
}];

注意:上面建立的方法內部程式碼主要歸結於建立一個整合於RACSignal的子類—>RACDynamicSignal,然後
通過靜態方法例項化出來,並把傳進去的任務Block進行物件屬性的儲存

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    RACDynamicSignal *signal = [[self alloc] init];
    signal->_didSubscribe = [didSubscribe copy];
    return [signal setNameWithFormat:@"+createSignal:"];
}

2.建立的訊號RACSignal(子類RACDynamicSignal)來呼叫第二步[Signale subscribeNext:^{}];來進行訊號的訂閱,內部轉換程式碼

RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
    return [self subscribe:o];

注意:這裡RAC的設計者向我們隱藏了RACSubscriber,對外暴露了RACSignal,所有的內部工作都由RACSubsriber進行完成傳遞
如果你再點進去,這裡傳遞的Signal,會判斷之前建立的時候傳進的Block是否為空,如果有任務,直接回撥Block

RACDisposable *innerDisposable = self.didSubscribe(subscriber);

3.現在有人訂閱了,又回撥了Block,然後觸發任務,完成之後呼叫

[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];

// 內部程式碼最終呼叫方法如下
- (void)sendNext:(id)value {
    @synchronized (self) {
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;

        nextBlock(value);
    }
}
最終回撥到訂閱的時候NextBlock的任務

4.這個可有可無,返回一個RACDisposable,對訂閱取消進行資源的釋放

總結:把他比喻成工廠,當你需要開啟生產流水線的時候(建立訊號,帶有任務),這個時候你工人都沒有,根本不會走你的任務
訊號不會被傳遞,而你有工人來的時候(就是訂閱了訊號),這個時候流水線才開始進行加工,這就是個人理解的冷訊號模式
也可以把冷訊號理解為未被訂閱的訊號,理解為訊號裡面的任務是不會進行的,只有訂閱者的加入,訊號才會變成熱訊號
也就是這玩意需要手動開啟訊號

RAC第二種的流程介紹—>RACSubject(繼承與RACSignal)

1.建立訊號
RACSubject *subject = [RACSubject subject];
該方法和上面的建立方式有所不同,他的例項化出來之後只是建立了一個陣列,專門用來儲存訂閱訊號的訂閱者


2.訂閱訊號

[subject subscribeNext:^(id x) {
// 當訊號sendNext的時候會回撥
NSLog(@"%@",x);
}];
// 這方法也是和上面的有所區別,RACSubject該物件會把訂閱者放到之前建立的陣列裡面,然後啥都不做了

3.[subject sendNext:value];

內部程式碼
[self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) {
        [subscriber sendNext:value];
    }];
可以看出,當他呼叫sendNext的時候,是會進行陣列的遍歷,然後挨個對訂閱者傳送訊息

總結:其實這就是所謂的熱訊號模式,還是拿工廠來做比喻,RACSubject是不管你有沒有人訂閱,我工廠24小時開啟流水線
我管你有沒有人加工,有人來了,我就用陣列登記一下,訊號來了的時候你們就負責接收任務,沒人的時候我還是就好比我的員工
花名冊是空的,但是照樣生產,只是沒人做事罷了,那麼這裡的RAC訊號流就是沒人處理罷了,會被丟棄

知識點:區別RACSubject和RACSignal
1.我個人理解,前者是冷訊號模式,需要有人訂閱才開啟熱訊號,後者是熱訊號預設,不管你有沒有訂閱

2.前者其實是一旦有人訂閱,就會去做對應的一組訊號任務,然後進行回撥,可以理解為有人的時候任務啟動,沒人的時候掛機
沒錯,我是把它簡單理解為代理,後者是熱訊號,訊號負責收集訂閱者陣列,發訊號的時候回遍歷訂閱者,一個個執行任務
你可以把它理解為通知,我管你有沒有接收,我照樣傳送,沒人就丟棄

3.前者個人用來進行網路請求,後者進行類似代理或者通知的資料傳遞模式,這樣就可以簡單的理解為,RAC其實就是把apple的一套
delegate,Notification,KVO等一系列方法綜合起來了,用起來更舒服罷了

4.那麼MVVM模式下,本身就多了個ViewModel,互動起來需要更多的設計模式協助,RAC就解決了這個問題,直接用這個設計模式來搞
資料傳遞和監聽的程式碼就清晰很多了


既然已經瞭解了RAC的流程,Demo走起!!!

MVVM + RAC示例Demo

1.用到了網路請求的訊號傳遞

2.用RACObserve巨集進行屬性的KVO觀察

3.用RACSubject進行資料的回撥

4.用RACSequence進行非同步陣列和字典(列印的是RACTuple)的遍歷

5.RAC–>combineLatest的方法進行簡單多輸入框登入註冊頁面模擬


先看下Demo裡面各個類的關係
這裡寫圖片描述

1.首先看下用MVVM的基類(MKJBaseViewController)

// baseVC的基礎ViewModel
// 子類重寫就能覆蓋型別
@property (nonatomic,strong,readonly) MKJBaseViewModel *viewModel;

/**
 唯一初始化方法

 @param viewModel 傳入ViewModel
 @return 例項化控制器物件
 */
- (instancetype)initWithViewModel:(MKJBaseViewModel *)viewModel;

/**
 佈局UI 子類重寫
 */
- (void)setupLayout;

/**
 請求網路資料 繫結資料 子類重寫
 */
- (void)setupBinding;

/**
 設定資料回撥,點選事件處理  子類重寫
 */
- (void)setupData;

初始化的時候
MKJDemoViewModel *viewModel = [[MKJDemoViewModel alloc] init];
            MKJDemoViewController *demoVC = [[MKJDemoViewController alloc] initWithViewModel:viewModel];
            [self.navigationController pushViewController:demoVC animated:YES];

2.再看一下TableViewController的基類
基礎的實現和普通擁有tableView的控制器一樣,無非區別在於代理的邏輯和資料交給了ViewModel

// 交給子類實現,傳遞最終的cell類
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath
{
    @throw [NSException exceptionWithName:@"抽象方法未實現"
                                   reason:[NSString stringWithFormat:@"%@ 必須實現抽象方法 %@",[self class],NSStringFromSelector(_cmd)]
                                 userInfo:nil];
}


#pragma mark - tableView datasource
// 交給ViewModel去實現
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.viewModel numberOfSections];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.viewModel numberOfRowInSection:section];
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return [self.viewModel heightForHeaderInSection:section];
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    return [self.viewModel viewForHeaderInSection:section];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MKJBaseTableViewCell *cell = [[self cellClassForRowAtIndexPath:indexPath] cellForTableView:tableView viewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
    cell.selectionStyle = [self.viewModel tableViewCellSelectionStyle];
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CGFloat height = tableView.rowHeight;
    NSNumber *calculateHeight = [[self cellClassForRowAtIndexPath:indexPath] calculateRowHeightWithViewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
    if (calculateHeight) {
        height = calculateHeight.floatValue;
    }
    return height;

}

3.最終上層的ViewController核心程式碼,最終剩下就這麼點程式碼了,瘦身不。。。。

// 基本佈局程式碼,順便設定個RACObserve
- (void)setupLayout
{
    [super setupLayout];
    @weakify(self);
    [RACObserve(self.viewModel, isNeedRefresh) subscribeNext:^(id x) {
        @strongify(self);
        if ([x boolValue]) {
            [self.tableView reloadData];
        }
    }];

}
// ViewModel進行網路請求
- (void)setupBinding
{
    [super setupBinding];
    @weakify(self)
    [self.viewModel sendRequest:^(id entity) {
        @strongify(self);
        [self hideLoadingViewFooter];
        [self.tableView reloadData];

    } failure:^(NSUInteger errCode, NSString *errorMsg) {

    }];
}
// 返回對應的CellClass
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [MKJDemoTableViewCell class];
}

4.來看下ViewModel在做什麼,核心還是網路請求,RAC訊號流的網路請求

// 網路請求外部
- (void)sendRequest:(MKJRequestSucceed)succeedBlock failure:(MKJRequestFailure)failBlock
{
    [[self.model requestDemoDatasWithPage:[self.currentPage integerValue] maxTime:self.currentMaxTime] subscribeNext:^(id data) {
        if (data) {
            self.entity = data;
            [self handlePagingEntities:self.entity.list
                            totalCount:@(self.entity.info.count)
                    cellViewModelClass:[MKJDemoTableViewCellViewModel class]
                               maxTime:self.entity.info.maxtime];
        }
        !succeedBlock ? : succeedBlock(data);
    }];
}

// 網路請求內部
- (RACSignal *)getRequestWithURLString:(NSString *)URLString
                  parametersDictionary:(NSDictionary *)paraterDictionary
                     parserEntityClass:(Class)parseEntityClass
{
    // 根據非同步請求建立一個新的RACSinal
    @weakify(self)
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self);

        [self.httpHelper getRequestWithUrlString:URLString
                             parametersDictonary:paraterDictionary
                                     entityClass:parseEntityClass
                                   completeBlock:^(id data) {
                                       [subscriber sendNext:data];
                                       [subscriber sendCompleted];
        }];

        return nil;
    }];
}

根據之前上面介紹的RAC邏輯,外部註冊訂閱者的時候成為熱訊號,然後呼叫建立信
號的Block,完成網路請求之後sendNext進行回撥,設計如此,然後在ViewModel中
把Model組裝完畢,進行外部的TableView reload。然後再次呼叫代理方法的時候,
會再次進到ViewModel裡面獲取已經組裝好的資料返回給TableView的DataSource
,OK了

5.Cell也單獨配置了對應CellViewModel,就是在RAC網路請求回來之後,把實體Model,用CellViewModel來進行組裝,只是把之前裝資料Model的陣列,用來裝擁有資料模型的CellViewModel,明白一點,ViewModel擁有Model,就能搞定資料的邏輯處理





我TM的終於明白了,NO BB Show me the code了,這根本說不清楚,需要的同學還是直接看Demo吧,最終的邏輯轉換就是上面的MVVM效果圖,理解了就可以了,無非就是一種設計思想,不過加上RAC確實還不錯。。。

我個人也比較喜歡看Demo,需要的還是直接開擼吧
正確Demo地址

相關文章