MVVM的业务实践

主要讲述的是MVVM在一些具体场景的业务实践

1. 首先讲一点是当我们讲MVVM的时候很多人觉得 MVVM的主要作用是如何避免把View Controller 写成 Massive View Controller ,但其实MVVM并不只是简单的解决Controller(后面简称C)的臃肿的问题(实际上如果正确和严格地使用 MVC 架构也不会造成vc臃肿),MVVM 在业务中除了解决Controller臃肿过重的问题之外还有很多重要的优势

<1>MVVM的介绍

 当我们讨论app架构的时候,其实所想要解决的问题本质在于,我们要如何才能更清晰地管理“用户操作,模型变更,UI 反馈”这一“数据流动”的方式(这也是一直在强调的,如果不从根本上理解“数据流动”在 mvvm 中的角色,就有可能从vc的臃肿换成viewmodel的臃肿,或者更严重点多了一层胶水代码增多了, 并且依然没有解决vc的臃肿,反而在实际使用的更麻烦了。mvvm或者说数据流动的最重要也是最核心的一点就是使用数据绑定的技术,使得当viewmodel 变化时候,view也会随之变化,从而实现了将视图和逻辑分离

在我之前的项目经常会遇到 同一个大模块下不同的业务之间ui的展示是相近的或者完全一样,这样借助mvvm 的优势可以实现ui组件的复用 。

<2>MVVM的写法实践

mvvm的数据绑定是比较重要的的一个环节,所以我们在业务的基础上 总结大约5种数据绑定方式

1. 单向数据绑定

2.双向数据绑定

3.集合的数据绑定

4 执行过程绑定

5 错误处理

基本着五种绑定能够覆盖业务中的大部分的场景

我们举简单demo例子去讲解这五种操作(以下都是结合RAC ,mvvm也可以不用RAC)

1.单向绑定

我们的app 基本上都会有纯粹展示数据的业务,比如支付宝产看账单 ,查看余额,或者网易新闻中我们查看新闻详情 新闻列表等等,这是一种很常见也是相对比较简单的业务

单向绑定其实是view到viewmodel 或者viewmodel到view的过程,比如我们把viewmodel的一个属性值能够显示在相应的view上,单向绑定一般应用的场景是 view的属性直接和viewmodel的属性关联起来,这样子我们可以通过修改viewmodel的方式改变view的展示的数据值,

看下面代码

// UI定义

@interface  BalanceCell : MVVMBaseCell

@property (strong, nonatomic) UILabel *balanceLabel;

@end

// ViewModel定义

@interface BalanceViewModel : BaseViewModel

@property (strong, nonatomic) NSString *balance;

@end

// 数据绑定 (我们一般把数据绑定放在view中)

- (void)bindViewModel:(BalanceViewModel *)viewModel {

      RAC(self.balanceLabel, text) = RACObserve(viewModel, balance)

}

这样的的绑定就是我们修改viewmodel的mobile的时候 view的mobileLabel也会随之变化

2.双向数据绑定

双向比较复杂点,一方面将viewmodel的属性显示view上,一方面也会对view的数据改变并同步到viewmodel,

我们的app 基本上都会有注册的业务,基本上都是包含用户输入姓名和手机号的功能

// UI定义

@interface MobileCell : MVVMBaseCell

@property (strong, nonatomic) UITextField *mobileTextField;

@end

// ViewModel定义

@interface MobileViewModel : BaseViewModel

@property (strong, nonatomic) NSString *mobile;

@end

// 数据双向绑定 我们一般把数据绑定放在view中)

- (void)bindViewModel:(MobileViewModel *)viewModel {

        RACChannelTerminal *channelTerminal = self.mobileTextField.rac_newTextChannel;

        RACChannelTerminal *channelTerminal2 = RACChannelTo(viewModel, mobile);        

        [channelTerminal subscribe:channelTerminal2];

        [channelTerminal2 subscribe:channelTerminal];

}

3.组合数据绑定

组合数据一般常见于多个组件之间的联动,换句话说就是他操作不是单个数据源 而是多个数据源合并的结果,举例房贷金融计算器的业务 贷款的结果会根据你输入的贷款期限,贷款金额,还款方式(等额本金还是等额本息)而变化, 看下面代码

// 信号组合

- (void)combineAmountViewModel:(AmountViewModel *)amountVM

                          timeViewModel:(TimeViewModel *) timeVM

                         methodViewModel:(MethodViewModel *)methodVM 

{

         NSArray *signals = @[amountVM.amount,timeVM.time,methodVM.method];

        _textSignal = [RACSignal combineLatest:signals

                                                                 reduce:^id (NSString *amount, NSString *time, NSString *method) 

        {

                    return [NSString stringWithFormat:@"计算结果 = 总贷款%@ 还款期限:%@ 还款方式%@ ",amount,time,method]}];

        }

}

4.执行过程绑定

执行过程绑定一般跟按钮或者cell的点击操作有关,我们会把按钮或者cell的点击操作行为和对应的代码关联绑定起来,这其实就是执行过程绑定。举个例子 我们经常在一个tableview 列表中 左滑删除一个cell 导致view的列表的的数量有所改变,或者我们还以注册的,一般会有点击清空的按钮 ,看下面代码

// UI定义

@interface OrderDetailCell : MVVMBaseCell

@property (strong, nonatomic) UIButton *clearButton;

@end

// ViewModel定义

@interface OrderDetailViewModel : BaseViewModel

@property (strong, nonatomic) RACCommand *clearCommand;

@end

// 执⾏信号创建

_clearCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

        nameVM.name = nil;

        phoneVM. phone= nil;

         return [RACSignal empty];

}];

  // 操作绑定

self.clearButton.rac_command = viewModel.clearCommand;

5.错误处理

错误处理会把一些不同的来源或者不同的错误处理,我们结合上面的例子的执行绑定扩展一下,在信号创建中我们加了一些判断,我们都会再点击提交的时候做一个判断(比如用户名或者手机号为空的时候)提示一个错误信息。

// 错误信号创建

_submitCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

        if (nameInputVM.inputText == nil) {

              NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"姓名不能为空!"};

               NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:-1 userInfo:userInfo];

               return [RACSignal error:error];

        } else if (phoneInputVM.inputText == nil) {

              NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"手机号不能为空!"};

            NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:-1 userInfo:userInfo];

            return [RACSignal error:error];

        }

        return [RACSignal empty];

}];

  // 信号绑定

self.submitButton.rac_command = viewModel.submitCommand;

@weakify(self)

[self.submitButton.rac_command.errors subscribeNext:^(NSError *x) {

    @strongify(self)

    self.textView.text = x.localizedDescription;

}];

<3>MVVM的代码复用

看上面的图,这里面是两个不用的业务一个是 TVL 一个是HTL, 从左往右看,但我们看到view层是公共的 ,我们看viewmodel 这个模块,可以看到viewmodle 根据业务分成两个部分,但是通用一个baseviewmodel ,baseviewmodel是这里面是关于ui的一些属性(跟业务逻辑无关)


还是刚才的例子 注册的时候其实有两个cell(都包含一个label 和一个textfiled),但是他们校验逻辑有不一样(一个是校验姓名逻辑还有一个校验手机号逻辑)那其实这个cell是可以复用的

<4>MVVM的自动化测试

这边讲的如何针对viewmodel层自动化测试,mvvm的另一个好处就是基于上面的数据绑定,我们只需要测试到viewmodle这一层就是覆盖到ui上的体现

我们举个例子 看下面代码

_verifyPhoneSignal = [RACObserve(self, inputText) map:^id (NSString *phone) {

    NSString *phoneRegexp = @"^1(3[0-9]|5[0-35-9]|8[0-25-9])\\d{8}$";

     NSPredicate *regextestmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", phoneRegexp];

    return @((BOOL)[regextestmobile evaluateWithObject:phone]);

}];

// 测试“校验⼿手机号”逻辑是否正确

- (void)testVerifyPhone {

    PhoneInputViewModel *viewModel = [[PhoneInputViewModel alloc] init];

    RACChannel *channel = [[RACChannel alloc] init];

    [viewModel.inputChannel subscribe:channel.leadingTerminal];

    [channel.leadingTerminal subscribe:viewModel.inputChannel];

    [channel.followingTerminal sendNext:@"18612345678"]; // 模拟从⽂文本框输⼊入 18612345678

    NSNumber *verifyPhoneResult = [viewModel.verifyPhoneSignal first];

    XCTAssertEqualObjects(verifyPhoneResult, @(YES));

    [viewModel setValue:@"13810001000" forKey:@"inputText"]; // 模拟ViewModel更更新phone值为13810001000

    XCTAssertEqualObjects([channel.followingTerminal first], @"13810001000"); // 检验⽂文本框内容是否为13810001000

}


<5>MVVM without RAC

使用 MVVM 最舒服的姿势是搭配 ReactiveCocoa(目前了解的 美团,知乎,蘑菇街都是采用MVVM + RAC),但是RAC 的学习成本和侵入性都比较高,这点就会导致已经开发人数比较多有自己固定的开发模式的团队中很难推起来了,所以如何不借助 ReactiveCocoa 来实现 MVVM。

RAC 是基于 KVO 构建的,所以也可以用 KVO 来让 VC 获取 VM 的变化。

但我们都知道 KVO 的槽点比较多,比如使用起来不方便,用完还要记得移除等。这里可以使用 Facebook 开源的 KVOController,它比较好的处理了 KVO 存在的一些问题,同时又能发挥 KVO 带来的便捷性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容