MVVM和ReactiveObjC实践2018

尝试在工程中使用MVVMReactiveObjC
引入MVVM是为了将界面和逻辑分开,将界面接口,比如label.text,转化为相应view model的数据属性接口。
引入ReactiveObjC主要是为了优雅地进行Controllerview model之间的双向绑定。
关于ReactiveObjC,这篇文章很有用:
iOS开发之ReactiveCocoa下的MVVM

目标页面

登录模块是基础模块之一。

  • 以验证码方式登录
企业微信截图_055f0b32-c210-4fec-8b5a-5184660c26ad.png
  • 以账号密码方式登录
企业微信截图_38ced2ef-74ff-4b01-a311-47a58bbb1715.png
  • 两种登录方式可以切换,在同一个界面。并且两种方式的共同点很少。

  • 当前的阶段:需求分析和页面交互已经评审通过了,但是后台接口还没有出来。

实现方式

  • 可以代码写界面,这个无话可说。只是界面代码真的很无聊,所以不选。

  • 用一套界面实现,一个组件会充当两种功能,比如同一个输入框,一会儿是手机号,一会是账号,逻辑比较复杂。这样做,还不如直接代码写界面。

  • 这个页面,上面部分是固定的,下面部分可变。可以考虑将下面部分分成两个位置重叠的view。这个方案比上面两个都好多了,不过界面重合在一起,看不清楚,故事版所见即所得的优势发挥不出来。

  • 引入Container View将重叠的部分平铺开来,感觉会好很多。 布局之后的样子大概是这样的:

企业微信截图_b7237bbf-a3c9-4fce-a8bb-d166224c62e3.png
  • 基本上已经很像了,只是现在还不能切换。剩下的就需要代码来动态控制了。

  • 引入Container View之后,复用级别就从view改成了controller,形成12子三个controller。和子view类似,子controller可以由父controller持有,从而建立相互之间的关系。

#import "KJTPasswordLoginChildViewController.h"
#import "KJTCodeLoginChildViewController.h"

@interface KJTLoginViewController ()

// child controller
@property (strong, nonatomic) KJTPasswordLoginChildViewController *passwordController;
@property (strong, nonatomic) KJTCodeLoginChildViewController *codeController;

@end

#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
    if ([segue.identifier isEqualToString:@"toPassword"]) {
        self.passwordController = segue.destinationViewController;
    }
    if ([segue.identifier isEqualToString:@"toCode"]) {
        self.codeController = segue.destinationViewController;
    }
}

引入MVVM

  • 引入MVVM会增加文件数量,这是不好的地方。

  • 将界面和逻辑分开,给controller减负,这是好的地方。

  • 将界面元素接口,转化为view model的数据属性接口,这是好的地方。

  • 现在后台接口文档还没出来,接口的名字和字段都不知道,引入一个view model,就可以把整个交互逻辑串起来了,这是好的地方。

  • 等以后接口文档好了,接口名字和接口字段定了,只要做一下接口字段到view model的属性名字映射就可以了,界面不用改动。这种解耦的方式,很适合现在的场景。

结论:综合考虑,对于这个登录页面,在当前的场景下,引入MVVM是有利的。

文件结构

  • 在交互图上,是一个界面,只是通过类似的tab切换,是界面重合,导致开发复杂化。为了很好地利用故事版所见即所得的优势,我们用3controller来描述这一个界面。

  • 并没有规定说一个controller要配一个独立的view model。虽然是3个不同的controller,但是实际上,可以把他们看做是一个。对于这3个强关联的controller,完全可以共用一个view model

  • 将上面所讲的两个观点通过文件夹结构来提现,大概是这个样子的:

企业微信截图_b45e7e8f-29ef-49fc-a694-de93a8dd0ce1.png

引入ReactiveObjC

  • ReactiveObjC是大名鼎鼎的Reactive CocoaObject-C版本

  • 一提到ReactiveObjC,就提到函数式编程,说是虽然效果好,但是学习曲线很陡峭,让很多人陷入纠结。其实,ReactiveObjC不需要函数编程,也是能够使用的。

  • ReactiveObjC把所有的交互方式都统一成了信号,感觉很高深的样子。其实想点击相应,消息什么的,原生的API已经很好用,并需要转换。

  • 如果不用ReactiveObjC,那么从view model来更新界面这一块就麻烦。首先要提供一个类似updateInterface之类的函数,然后,在需要改变界面的时候调用,很繁琐

  • 如果用ReactiveObjC,那么可以建立controllerview model之间的属性绑定,不需要到处调用updateInterface,感觉好多了。

  • 在这里,只需要用到RACRACObserve,真的很简单。

结论:引入ReactiveObjC,建立controllerview model之间的属性绑定,当view model改变的时候,可以优雅地在界面上提现出来。

View Model定义

  • view model是用来描述界面的,相当于界面的接口。这里,我们只用来描述父controller

  • 这里的界面元素可以抽象为以下几种:

  1. 文字的颜色,选中是蓝色的,不选中是黑色的;
  2. 指示线的显示和隐藏;
  3. 代表子controllercontainer view的显示和隐藏;
  4. 当前用户选择了那种登录方式;
  • 通过数据属性,来描述上面提到的几点界面特征,定义如下:
typedef NS_ENUM(NSInteger,KJTLoginType) {
    KJTLoginTypeCode = 1,
    KJTLoginTypePassword = 2,
};

@interface KJTLoginViewModel : NSObject

/*
 *  KJTLoginViewController
 */

// 登录方式
@property (assign, nonatomic) KJTLoginType loginType;

// 验证码标签颜色
@property (strong, nonatomic) UIColor *codeLabelColor;
// 隐藏验证码标签横线
@property (assign, nonatomic) BOOL hideCodeLine;
// 隐藏验证码登录容器
@property (assign, nonatomic) BOOL hideCodeContainer;

// 账号密码标签颜色
@property (strong, nonatomic) UIColor *passwordLabelColor;
// 隐藏账号密码标签横线
@property (assign, nonatomic) BOOL hidePasswordLine;
// 隐藏账号密码登录容器
@property (assign, nonatomic) BOOL hidePasswordContainer;

/*
 *  KJTPasswordLoginChildViewController
 */


/*
 *  KJTCodeLoginChildViewController
 */

@end
  • 登录方式和其他的属性是有关系的,所以在创建view model的时候就可以将这种关系固定下来。这里使用ReactiveObjC会显得非常优雅。

  • 建立了登录方式和其他属性之间的关系之后,默认值就非常方便了,只要设置登录方式一项就可以了,其他的,自然有ReactiveObjC对应设置,非常方便。

  • 引入ReactiveObjC之后,view model的代码可以写得非常优雅:

#import "KJTLoginViewModel.h"

@implementation KJTLoginViewModel

#pragma mark - life cycle
- (instancetype)init {
    self = [super init];
    if (self) {
        [self bindLoginType];
        [self setDefaultValue];
    }
    return self;
}

#pragma mark - private
- (void)bindLoginType {
    [RACObserve(self, loginType) subscribeNext:^(id  _Nullable x) {
        KJTLoginType loginType = (KJTLoginType)[x integerValue];
        switch (loginType) {
            case KJTLoginTypeCode:
                self.codeLabelColor = kBlueColor;
                self.hideCodeLine = NO;
                self.hideCodeContainer = NO;
                self.passwordLabelColor = kBlackColor;
                self.hidePasswordLine = YES;
                self.hidePasswordContainer = YES;
                break;
            case KJTLoginTypePassword:
                self.codeLabelColor = kBlackColor;
                self.hideCodeLine = YES;
                self.hideCodeContainer = YES;
                self.passwordLabelColor = kBlueColor;
                self.hidePasswordLine = NO;
                self.hidePasswordContainer = NO;
                break;
            default:
                break;
        }
    }];
}

- (void)setDefaultValue {
    self.loginType = KJTLoginTypePassword;
}

@end

控制器属性

  • 输出口:故事版只能表达静态页面,可变的动态页面,需要拉输出口到controller中,进行动态控制。

  • view model:作为controller的一个属性成员,为controller处理交互逻辑,页面逻辑方面的工作。

  • 属性的定义大概是这个样子的:

@interface KJTLoginViewController ()
// 验证码登录
@property (weak, nonatomic) IBOutlet UILabel *codeLabel;
@property (weak, nonatomic) IBOutlet UIView *codeLine;
@property (weak, nonatomic) IBOutlet UIView *codeContainer;
// 账号密码登录
@property (weak, nonatomic) IBOutlet UILabel *passwordLabel;
@property (weak, nonatomic) IBOutlet UIView *passwordLine;
@property (weak, nonatomic) IBOutlet UIView *passwordContainer;
// view model
@property (strong, nonatomic) KJTLoginViewModel *viewModel;

@end

绑定View Model

view model的属性变化要反映在界面上,那么就需要建立代表界面的输出口和view model属性之间的绑定工作。有ReactiveObjC的帮助,这将会非常简单:

#pragma mark - private
- (void)bindViewModel {
    self.viewModel = [[KJTLoginViewModel alloc] init];
    RAC(self.codeLabel, textColor) = RACObserve(self.viewModel, codeLabelColor);
    RAC(self.codeLine, hidden) = RACObserve(self.viewModel, hideCodeLine);
    RAC(self.codeContainer, hidden) = RACObserve(self.viewModel, hideCodeContainer);
    RAC(self.passwordLabel, textColor) = RACObserve(self.viewModel, passwordLabelColor);
    RAC(self.passwordLine, hidden) = RACObserve(self.viewModel, hidePasswordLine);
    RAC(self.passwordContainer, hidden) = RACObserve(self.viewModel, hidePasswordContainer);
}

登录方式切换

由于界面的元素输出口已经和view model的属性进行了绑定; view model的其他属性已经和登录方式属性进行了绑定。所以,当用户切换登录方式时,只要修改view model的登录方式属性loginType就可以,简单易懂:

- (IBAction)codeButtonTouched:(id)sender {
    self.viewModel.loginType = KJTLoginTypeCode;
}

- (IBAction)passwordButtonTouched:(id)sender {
    self.viewModel.loginType = KJTLoginTypePassword;
}

运行效果

  • 由于view model中的默认值是以“账号密码方式登录”,所以默认界面是这样的:
- (void)setDefaultValue {
    self.loginType = KJTLoginTypePassword;
}
企业微信截图_8ed75d18-9d81-4481-a48c-212b716dda68.png
  • 切换到“验证码方式登录”,界面是这样的:
企业微信截图_43323917-8421-40f4-9317-2e832ca2a294.png

小结: 引入ReactiveObjC之后,以前相对比较繁琐的“tab切换”页面,可以非常简洁的实现。

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

推荐阅读更多精彩内容