iOS開發(fā):ReactiveCocoa+MVVM(UITableView)實(shí)戰(zhàn)

前言

上一篇文章中,筆者簡(jiǎn)單的閱讀了ReactiveCocoa官方文檔穆趴,了解了ReactiveCocoa的基本使用后饿凛。這篇文章主要探討一下,如何基于MVVM的設(shè)計(jì)模式在含有UITableView界面中使用RAC綁定數(shù)據(jù)剖笙。

MVVM

MVVM本質(zhì)上是基于MVC的一個(gè)改進(jìn)版卵洗,它是在傳統(tǒng)MVC模式上添加了一個(gè)ViewModel。ViewModel可以取出 Model 的數(shù)據(jù)同時(shí)幫忙處理 View 中由于需要展示內(nèi)容而涉及的業(yè)務(wù)邏輯弥咪,為Controller減壓过蹂。

MVVM

實(shí)戰(zhàn)

下面筆者將基于ReactiveCocoa+MVVM+UITableView,實(shí)現(xiàn)一個(gè)常見的列表展示聚至,以及按鈕點(diǎn)擊改變cell中的字體顏色榴啸。
源碼地址:
https://github.com/pengwj/blogWork/tree/master/code/ReacttiveObjC-MVVM-tableview

Controller

移除網(wǎng)絡(luò)請(qǐng)求以及數(shù)據(jù)處理后的Controller異常的簡(jiǎn)潔。我看很多人將tablevie的delegate晚岭、datasoure代理方法都放到了ViewModel中鸥印,但是考慮到這樣的話ViewModel就綁定了View勋功,所以最終決定將代理方法都放在VC中,具體可以下載我的代碼看看库说。

// 核心代碼如下
- (void)initViewModel {
    
    _viewModel = [MainViewModel new];
    @weakify(self)
    [_viewModel.fetchProductCommand.executing subscribeNext:^(NSNumber *executing) {
        NSLog(@"command executing:%@", executing);
        if (!executing.boolValue) {
            @strongify(self)
            [self.tableView.mj_header endRefreshing];
        }
    }];
    
    [_viewModel.fetchMoreProductCommand.executing subscribeNext:^(NSNumber *executing) {
        if (!executing.boolValue) {
            @strongify(self);
            [self.tableView.mj_footer endRefreshing];
        }
    }];
    
    [_viewModel.errors subscribeNext:^(NSError *error) {
        NSLog(@"something error:%@", error.userInfo);
        //TODO: 這里可以選擇一種合適的方式將錯(cuò)誤信息展示出來(lái)
    }];
    
}

- (void)bindViewModel {
    @weakify(self);
    
    [RACObserve(self.viewModel, dataArray) subscribeNext:^(id x) {
        @strongify(self);
        
        NSLog(@"bindViewModel-dataArray");
        [self.tableView reloadData];
    }];
    
}

Model

model層相對(duì)MVC模式?jīng)]有改變狂鞋,只是簡(jiǎn)單的賦值操作。

//MainModel.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface MainModel : NSObject

@property (nonatomic, copy) NSString *rtTitle;
@property (nonatomic, copy) NSString *rtSelect;

+ (instancetype)mainModelWithDic:(NSDictionary *)dic;

@end

NS_ASSUME_NONNULL_END
// MainModel.m
+ (instancetype)mainModelWithDic:(NSDictionary *)dic
{
    MainModel *model = [[MainModel alloc] init];
    model.rtTitle = dic[@"title"];
    model.rtSelect = dic[@"select"];
    
    return model;
}

View

View中主要做了數(shù)據(jù)綁定潜的,監(jiān)聽了Model以及一些控件響應(yīng)事件

// MainCell.m
- (void)bindData
{
    @weakify(self)
    [RACObserve(self, model.rtTitle) subscribeNext:^(id x) {
        
        @strongify(self)
        self.rtLabel.text = self.model.rtTitle;
        
    }];
    
    [RACObserve(self, model.rtSelect) subscribeNext:^(id x) {
        
        if ([self.model.rtSelect isEqualToString:@"0"]) {
            self.rtLabel.textColor = [UIColor yellowColor];
        } else {
            self.rtLabel.textColor = [UIColor redColor];
        }
    }];
    
    self.rtButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){
        NSLog(@"按鈕被點(diǎn)擊");
        
        if ([self.model.rtSelect isEqualToString:@"0"]) {
            self.model.rtSelect = @"1";
        } else {
            self.model.rtSelect = @"0";
        }
        
        return [RACSignal empty];
    }];
}

ViewModel

ViewModel中需要實(shí)例化網(wǎng)絡(luò)請(qǐng)求的RARACCommand骚揍,以及監(jiān)聽網(wǎng)絡(luò)請(qǐng)求回來(lái)的數(shù)據(jù),并對(duì)數(shù)據(jù)做相應(yīng)的邏輯處理啰挪。

- (void)initCommed
{
    _dataArray = [NSMutableArray arrayWithCapacity:0];
    
    @weakify(self)
    _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        @strongify(self)
        self.pageIndex = 0;
        return [[[APIClient sharedClient]
                        fetchProductWithPageIndex:self.pageIndex]
                        takeUntil:self.cancelCommand.executionSignals];
    }];
    
    _fetchMoreProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        self.pageIndex = self.pageIndex+1;
        return [[[APIClient sharedClient] fetchProductWithPageIndex:self.pageIndex] takeUntil:self.cancelCommand.executionSignals];
    }];
    
}

- (void)initSubscribe {
    
    @weakify(self);
    [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(id responseObject) {
        @strongify(self);
        
        NSDictionary *responseDict = (NSDictionary *)responseObject;
        
        NSInteger code = [[responseDict objectForKey:@"code"] integerValue];
        
        if (code == 0) {
            
            NSArray *infoArray = [responseDict objectForKey:@"info"];
            NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:0];
            
            [infoArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull dic,NSUInteger idx,BOOL * _Nonnull stop){
                
                MainModel *model = [MainModel mainModelWithDic:dic];
                [tempArray addObject:model];
                
            }];

            /// ??????注意這里需要通過(guò)KVC的方式對(duì)數(shù)組進(jìn)行操作
            NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.dataArray.count, [tempArray count])];
            [[self mutableArrayValueForKey:@"dataArray"] insertObjects:tempArray atIndexes:indexSet];
            
        } else {
            
            [self.errors sendNext:[NSError new]];
        }

    }];
    
    [[_fetchMoreProductCommand.executionSignals switchToLatest] subscribeNext:^(id responseObject) {
        @strongify(self);
        
        NSDictionary *responseDict = (NSDictionary *)responseObject;
        NSInteger code = [[responseDict objectForKey:@"code"] integerValue];
        
        if (code == 0) {
            
            NSArray *infoArray = [responseDict objectForKey:@"info"];
            NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:0];

            [infoArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull dic,NSUInteger idx,BOOL * _Nonnull stop){
                
                MainModel *model = [MainModel mainModelWithDic:dic];
                [tempArray addObject:model];

            }];
            
            /// ??????注意這里需要通過(guò)KVC的方式對(duì)數(shù)組進(jìn)行操作
            NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.dataArray.count, [tempArray count])];
            [[self mutableArrayValueForKey:@"dataArray"] insertObjects:tempArray atIndexes:indexSet];
            
        } else {
            
            [self.errors sendNext:[NSError new]];
        }
    }];
    
    [[RACSignal merge:@[_fetchProductCommand.errors, self.fetchMoreProductCommand.errors]] subscribe:self.errors];
}

遇到的坑

直接獲取NSMutableArray進(jìn)行操作不會(huì)觸發(fā)RAC的監(jiān)聽信號(hào)信不,所以這里需要通過(guò)KVC來(lái)添加、移除數(shù)據(jù)亡呵。

// 通過(guò)KVC獲取數(shù)組
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// eg 
// 正確的例子
[[self mutableArrayValueForKey:@"dataArray"] addObjectsFromArray:tempArray];

// 錯(cuò)誤的例子
[self.dataArray addObjectsFromArray:tempArray];

總結(jié)

可能是由于這個(gè)是前幾年比較火的技術(shù)抽活,我在網(wǎng)上查找了很多資料都沒有一個(gè)比較完整、統(tǒng)一的實(shí)現(xiàn)方法锰什。主要集中如下:

  1. UITableView的代理方法下硕、數(shù)據(jù)源回調(diào)方法是否需要封裝到ViewModel中
  2. dataArray數(shù)據(jù)存儲(chǔ)數(shù)組是放在ViewModel中,還是放在VC中
  3. Cell的數(shù)據(jù)綁定理應(yīng)是應(yīng)用RAC監(jiān)聽Model的變化汁胆,但是部分文章的實(shí)現(xiàn)依然是主動(dòng)調(diào)用方法去刷新
    最終筆者選擇了一個(gè)還比較符合心目中對(duì)MVVM理解的文章進(jìn)行學(xué)習(xí)梭姓,然后實(shí)現(xiàn)了這個(gè)demo。

ReactiveCocoa發(fā)展之際可能剛好遇到swift橫空出世嫩码,導(dǎo)致RAC在OC還沒有完全普及誉尖,大量開發(fā)者就轉(zhuǎn)向swift以及RxSwift了。目前網(wǎng)上搜索RAC的文章都是幾年前的铸题,而RxSwift則都是最新的文章释牺。這里在此立個(gè)flag,要開始學(xué)習(xí)swift啦回挽。

推薦閱讀:

文章

RACObserving an NSMutableArray
一次MVVM+ReactiveCocoa實(shí)踐
ReactiveCocoa+MVVM實(shí)戰(zhàn)
iOS如何為NSMutableArray添加KVO

源碼

MVVMReactiveCocoa
MVVMReactiveCocoaDemo
MVVMDemo
MVVMReactiveCocoa

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末没咙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子千劈,更是在濱河造成了極大的恐慌祭刚,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墙牌,死亡現(xiàn)場(chǎng)離奇詭異涡驮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)喜滨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門捉捅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人虽风,你說(shuō)我怎么就攤上這事棒口〖脑拢” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵无牵,是天一觀的道長(zhǎng)漾肮。 經(jīng)常有香客問我,道長(zhǎng)茎毁,這世上最難降的妖魔是什么克懊? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮七蜘,結(jié)果婚禮上谭溉,老公的妹妹穿的比我還像新娘。我一直安慰自己橡卤,他們只是感情好扮念,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蒜魄,像睡著了一般扔亥。 火紅的嫁衣襯著肌膚如雪场躯。 梳的紋絲不亂的頭發(fā)上谈为,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音踢关,去河邊找鬼伞鲫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛签舞,可吹牛的內(nèi)容都是我干的秕脓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼儒搭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吠架!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起搂鲫,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤傍药,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后魂仍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拐辽,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年擦酌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俱诸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赊舶,死狀恐怖睁搭,靈堂內(nèi)的尸體忽然破棺而出赶诊,到底是詐尸還是另有隱情,我是刑警寧澤介袜,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布甫何,位于F島的核電站,受9級(jí)特大地震影響遇伞,放射性物質(zhì)發(fā)生泄漏辙喂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一鸠珠、第九天 我趴在偏房一處隱蔽的房頂上張望巍耗。 院中可真熱鬧,春花似錦渐排、人聲如沸炬太。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)亲族。三九已至,卻和暖如春可缚,著一層夾襖步出監(jiān)牢的瞬間霎迫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工帘靡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留知给,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓描姚,卻偏偏與公主長(zhǎng)得像涩赢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子轩勘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容