iOS MVX 设计模式漫谈

开场白

说到架构,首先要了解什么是架构?
架构按照我的理解,是属于一种编程经验的集合和总结,在经过许多人许多年的使用之后,基本稳定下来的一种模式。通常具有以下优点:适用性强,实用性强,可复用,易修改
文章将使用登录的场景分别使用 MVC、MVP、MVVM 三种模式做示例,方便进行理解与对比。

各设计模式具体分析

---MVC---

大家一定反应肯定是 MVC 。MVC就是架构中最经典的一种。

M是指业务数据, V是指用户界面, C则是控制器. 在具体的业务场景中, C作为M和V之间的连接, 负责获取输入的业务数据, 然后将处理后的数据输出到界面上做相应展示, 另外, 在数据有所更新时, C还需要及时提交相应更新到界面展示.

另外在我看来,MVC 分为两种,一种是 Model 与 View 会相互影响,这种是经典的 MVC 架构、而另外一种就是苹果推荐的 MVC 结构,其中 Model 与 View 相互隔离,只通过 Controller 来更新。

在 MVC 或者 MVX 一系列的模式中,都可以把开发看作为搭积木,把 M 和 V 分离当作积木,而在 C 或者其他地方中进行拼接组合,将积木搭成想要的样子.

经典 MVC 苹果 MVC
View 与 Model 可以互相持有 View 与 Model 都不可以持有对方
View 和 Model 可以互相修改
也可以通过 Controller 修改
只能通过 Controller 修改

层次细分

M 层分为胖瘦两种,第一种瘦 Model,只负责数据的接收、分解和传递,可以对单个属性进行修改,也可以通过方法从字典中写入属性;另外一种是胖 Model,胖 Model 除了瘦 Model 的基本功能之外,还需要负责一部分的业务逻辑,包括但不限于:

  1. 数据的截取
  2. 时间戳转换为正常时间格式
  3. 增加前缀或者修正为需要的格式
  4. 数据格式的转换
  5. 对数据进行判断以及处理

V 层主要负责用户界面的组合以及界面属性等一系列与用户界面有关的功能,在经典的 MVC 中,可以通过暴露一个接收 Model 的方法来实现界面数据的修改。但在苹果的 MVC 中,由于无法引用 Model,则需要把一些需要进行改变的视图暴露出来或者定义额外的数据来实现界面数据的修改。(如果是暴露 UIView 的子类,则设置为 readonly;如果定义额外数据,则可以通过 get 和 set 方法修改界面数据。)

C 层的功能主要是对界面管理与业务逻辑处理,包括但不止以下内容:

  1. 对各 UI 视图进行排版、布局与适配;
  2. 实现所需要的点击事件、协议方法;
  3. 实现控制器之间的跳转;
  4. 实现网络请求,并对结果进行处理;
  5. 同时负责 V 层与 M 层的数据的修改。

MVC 示例

在这里以及后面的模式中,都将会通过实现视图页面来进行示例的展示。在示例中,会在一些需要难点和要点会通过注释来说明。

在编码之前,首先需要确认一下需要实现的功能有哪些:

  1. 界面由账号 TextField、密码 TextField、登录按钮组成
  2. 账号与密码都需要长度检测,长度为8-16位
  3. 密码需要格式检测(必须要有字母和数字)
  4. 登录请求与处理

所有的基本类似,所以需求只写一次

MVC 登录.png 以下是代码实现:

LoginModel.h
//
//  LoginModel.h
//  login
//

#import <Foundation/Foundation.h>

@interface LoginModel : NSObject
/// 用户名
@property (nonatomic, strong) NSString *userName;
/// 密码
@property (nonatomic, strong) NSString *passWord;
@end

LoginModel.m
//
//  LoginModel.m
//  login
//

#import "LoginModel.h"

@implementation LoginModel

@end

在 LoginModel 中,设计成瘦 Model ,如果需要改成胖 Model ,则可以把 C 中 check 相关的代码转移过来。

LoginView.h
//
//  LoginView.h
//  login
//

#import <UIKit/UIKit.h>

@interface LoginView : UIView
/// 用户名
@property (nonatomic, strong, readonly) NSString *userName;
/// 密码
@property (nonatomic, strong, readonly) NSString *passWord;

/// 登录回调
@property (nonatomic, copy) void (^loginBlock)();
@end

LoginView.m
//
//  LoginView.m
//  login
//

#import "LoginView.h"

@interface LoginView ()
/// 用户名输入框
@property (nonatomic, strong) UITextField *userNameTxt;
/// 密码输入框
@property (nonatomic, strong) UITextField *passWordTxt;
/// 登录按钮
@property (nonatomic, strong) UIButton *loginBtn;
@end
@implementation LoginView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self createUI];
    }
    return self;
}

/**
 创建视图
 */
- (void)createUI {
    [self addSubview:self.userNameTxt];
    [self addSubview:self.passWordTxt];
    [self addSubview:self.loginBtn];
    
    [self placeSubviews];
}

/**
 子视图布局
 */
- (void)placeSubviews {
    self.userNameTxt.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 44);
    self.passWordTxt.frame = CGRectMake(0, 50, CGRectGetWidth(self.frame), 44);
    self.loginBtn.frame = CGRectMake(20, 116, CGRectGetWidth(self.frame)-40, 44);
}

//MARK:- event response
/**
 登录点击
 */
- (void)loginEvent {
    if (self.loginBlock) {
        self.loginBlock();
    }
}

//MARK:- getter/setter
- (UITextField *)userNameTxt {
    if (!_userNameTxt) {
        _userNameTxt = [UITextField new];
        // 设置用户名基本样式
        _userNameTxt.font = [UIFont systemFontOfSize:15];
        _userNameTxt.borderStyle = UITextBorderStyleRoundedRect;
        _userNameTxt.placeholder = @"请输入用户名";
    }
    return _userNameTxt;
}

- (UITextField *)passWordTxt {
    if (!_passWordTxt) {
        _passWordTxt = [UITextField new];
        // 设置密码基本样式
        _passWordTxt.secureTextEntry = YES;
        _passWordTxt.font = [UIFont systemFontOfSize:15];
        _passWordTxt.borderStyle = UITextBorderStyleRoundedRect;
        _passWordTxt.placeholder = @"请输入密码";
    }
    return _passWordTxt;
}

- (UIButton *)loginBtn {
    if (!_loginBtn) {
        _loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        // 设置按钮的基本样式
        [_loginBtn setTitle:@"登录" forState:UIControlStateNormal];
        _loginBtn.titleLabel.font = [UIFont systemFontOfSize:15];
        [_loginBtn setBackgroundColor:[UIColor redColor]];
        [_loginBtn addTarget:self action:@selector(loginEvent) forControlEvents:UIControlEventTouchUpInside];
    }
    return _loginBtn;
}

- (NSString *)userName {
    return self.userNameTxt.text;
}

- (NSString *)passWord {
    return self.passWordTxt.text;
}
@end

LoginView 中,进行了视图的处理,以及将用户名和密码的字符暴露出来,并通过 block 来获取按钮的点击事件。

LoginViewController.h
//
//  LoginViewController.h
//  login
//

#import <UIKit/UIKit.h>

@interface LoginViewController : UIViewController

@end

LoginViewController.m
//
//  LoginViewController.m
//  login
//

#import "LoginViewController.h"

#import "LoginView.h"

#import "LoginModel.h"
@interface LoginViewController ()
/// 登录视图
@property (nonatomic, strong) LoginView *loginView;
/// 登录模型
@property (nonatomic, strong) LoginModel *loginModel;
@end

@implementation LoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createUI];
}

/**
 创建视图
 */
- (void)createUI {
    /// 这里的布局由于是 demo,所以是写死的,没有进行过适配,子页面中的也是一样
    [self.view addSubview:self.loginView];
}

//MARK:- netWork
- (void)loginRequest {
    // 进行网络请求 \
    成功保存账号密码 \
    失败弹框提示
}

//MARK:- event response
- (void)check {
    if ([self passWordCheck] && [self userNameCheck]) {
        // 没问题则进行网络请求
        [self loginRequest];
    }
    else {
        // 问题处理、提示
    }
}

- (BOOL)passWordCheck {
    // 同时包含大小写英文与数字且长度在8-16之间
    NSString *regex = @"^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    return [predicate evaluateWithObject:self.loginModel.passWord];
}

- (BOOL)userNameCheck {
    // 包含大小写英文与数字且长度在6-20之间
    NSString *regex = @"^[a-zA-Z0-9]{8,16}$";
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    
    return [predicate evaluateWithObject:self.loginModel.userName];
}


//MARK:- getter/setter
- (LoginView *)loginView {
    if (!_loginView) {
        _loginView = [[LoginView alloc] initWithFrame:CGRectMake(10, CGRectGetHeight(self.view.frame)-220, CGRectGetWidth(self.view.frame)-20, 160)];
        
        __weak typeof(self) weakSelf = self;
        _loginView.loginBlock = ^{
            __strong typeof(self) self = weakSelf;
            self.loginModel.userName = self.loginView.userName;
            self.loginModel.passWord = self.loginView.passWord;
            [self check];
        };
    }
    return _loginView;
}

- (LoginModel *)loginModel {
    if (!_loginModel) {
        _loginModel = [LoginModel new];
    }
    return _loginModel;
}
@end

Controller 中,负责 LoginView 的布局与展示,网络请求以及成功失败的处理,按钮点击事件的处理, View 与 Model 的交互等。


---MVP---

mvp的全称为Model-View-Presenter,Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

以上是经典的 MVP 模式,看起来是不是很熟悉?是不是看起来很像 MVC ?是的,在 iOS 中,推荐的 MVC 模式基本上和经典 MVP 相似(但是依旧是不一样的),而在 iOS 中,MVP 则是把 MVC 中的 Controller 进行拆分,分为 Presenter 和 Controller,所以我觉得应该叫做 MVCP 才对。

在 iOS 的 MVCP 模式中,M 是指业务数据, V 是指用户界面,P 处理业务逻辑和将数据写入界面,C 负责胶水粘合、控制器管理和视图管理。

MVP MVC
P 负责逻辑处理 C 负责逻辑处理和界面展示
在 P 中引用 V 和 M 在 C 中引用 V 和 M

层次细分

在这里的层次细分就比 MVC 中的寒碜很多了,因为 M 层和 V 层没有发生改变,就不重复介绍了:

M 层与 MVC 中的 M 层相同;
V 层与 MVC 中的 V 层相同;
P 层将原先 C 层中的部分业务功能拆分出来(关于控制器的跳转,还是放到 C 层中,尽量减少耦合),同时 P 层引用了 V 层和 M 层,并且 V 层和 M 层的修改与实现通过 P 层进行处理;
C 层在 P 层把功能拆分出来之后,只剩下了部分功能,包括:

  1. 胶水功能,P 层、M 层、V 层的链接都是通过 C 层进行
  2. 控制器之间的交互
  3. 视图管理

MVP 登录.png

MVP 示例

LoginPresenter.h
//
//  LoginPresenter.h
//  login
//

#import <UIKit/UIKit.h>

@class LoginView,LoginModel;
@interface LoginPresenter : NSObject
/// 登录视图
@property (nonatomic, strong) LoginView *loginView;
/// 登录模型
@property (nonatomic, strong) LoginModel *loginModel;
/// 登录失败回调
@property (nonatomic, copy) void (^loginSuccessBlock)();
/// 登录成功回调
@property (nonatomic, copy) void (^loginFailBlock)();
@end

LoginPresenter.m
//
//  LoginPresenter.m
//  login
//

#import "LoginPresenter.h"

#import "LoginView.h"

#import "LoginModel.h"
@implementation LoginPresenter

//MARK:- netWork
- (void)loginRequest {
    // 进行网络请求 \
    成功保存账号密码 \
    失败弹框提示
    if (/* success && */self.loginSuccessBlock) {
        self.loginSuccessBlock();
    }
    else if (/* fail && */self.loginFailBlock) {
        self.loginFailBlock();
    }
}

//MARK:- event response
- (void)check {
    if ([self passWordCheck] && [self userNameCheck]) {
        // 没问题则进行网络请求
        [self loginRequest];
    }
    else {
        // 问题处理、提示
    }
}

- (BOOL)passWordCheck {
    // 同时包含大小写英文与数字且长度在8-16之间
    NSString *regex = @"^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    return [predicate evaluateWithObject:self.loginModel.passWord];
}

- (BOOL)userNameCheck {
    // 包含大小写英文与数字且长度在6-20之间
    NSString *regex = @"^[a-zA-Z0-9]{8,16}$";
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    
    return [predicate evaluateWithObject:self.loginModel.userName];
}

//MARK:- getter/setter
- (void)setLoginView:(LoginView *)loginView {
    _loginView = loginView;
    __weak typeof(self) weakSelf = self;
    _loginView.loginBlock = ^{
        __strong typeof(self) self = weakSelf;
        self.loginModel.userName = self.loginView.userName;
        self.loginModel.passWord = self.loginView.passWord;
        [self check];
    };
}
@end

LoginViewController.m
//
//  LoginViewController.m
//  login
//

#import "LoginViewController.h"

#import "LoginView.h"

#import "LoginModel.h"

#import "LoginPresenter.h"
@interface LoginViewController ()
/// 登录视图
@property (nonatomic, strong) LoginView *loginView;
/// 登录模型
@property (nonatomic, strong) LoginModel *loginModel;

@property (nonatomic, strong) LoginPresenter *loginPresenter;
@end

@implementation LoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createUI];
}

/**
 创建视图
 */
- (void)createUI {
    /// 这里的布局由于是 demo,所以是写死的,没有进行过适配,子页面中的也是一样
    [self.view addSubview:self.loginView];
    
    self.loginPresenter.loginView = self.loginView;
    self.loginPresenter.loginModel = self.loginModel;
}

//MARK:- private method

/**
 登录成功
 */
- (void)loginSuccess {
    
}

/**
 登录失败
 */
- (void)loginFail {
    
}

//MARK:- getter/setter
- (LoginView *)loginView {
    if (!_loginView) {
        _loginView = [[LoginView alloc] initWithFrame:CGRectMake(10, CGRectGetHeight(self.view.frame)-220, CGRectGetWidth(self.view.frame)-20, 160)];
    }
    return _loginView;
}

- (LoginModel *)loginModel {
    if (!_loginModel) {
        _loginModel = [LoginModel new];
    }
    return _loginModel;
}

- (LoginPresenter *)loginPresenter {
    if (!_loginPresenter) {
        _loginPresenter = [LoginPresenter new];
        
        __weak typeof(self) weakSelf = self;
        _loginPresenter.loginSuccessBlock = ^{
            __strong typeof(self) self = weakSelf;
            [self loginSuccess];
        };
        
        _loginPresenter.loginFailBlock = ^{
            __strong typeof(self) self = weakSelf;
            [self loginFail];
        };
    }
    return _loginPresenter;
}
@end

MVP 和 MVC 相比,把业务逻辑单独提取到 P 层处理,方便问题的定位、测试和修改,但同时需要把控制器或者视图控制部分功能进行回调(因为是控制器的功能),但是缺点就是耦合度比较大,基本无法进行复用,其次就是会多出一个文件,如果所有的都转为 MVP ,则平均会多上1/4的文件,不方便管理。

---MVVM---

MVVM是Model-View-ViewModel的简写,MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构框架。它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化。

MVVM 就是 MVP 的升级版本,如果在之前已经对 MVP 有一定的了解,那么了解 MVVM 的过程将会变得简单而快捷。同时,跟 MVP 同理,MVVM 在 iOS 中,应该被称为 MVCVM。毕竟控制器是你永远都摆脱不了的。

在 iOS 的 MVCVM 模式中,M 是指业务数据, V 是指用户界面,VM 处理业务逻辑和将数据写入界面,C 负责胶水粘合、控制器管理和视图管理。

MVVM MVP
VM 负责业务数据的绑定和业务逻辑的处理 P 负责逻辑处理
V 层引用 VM,VM 引用 M层 P 层同时引用 V 层和 M 层

层次细分

在这里的层次细分就跟 MVP 中的差不多,主要还是引用和绑定的区别:

M 层与 MVC 中的 M 层相同;
V 层在 MVC 中的 V 层的基础上,加上 VM 层的引用,实现由 VM 的数据绑定导致的属性修改;
VM 层与 MVP 中的 P 层相类似,主要区别在于:

  1. VM 层只引用 M 层,同时对 M 层的数据进行绑定;
  2. V 层和 C 层引用 VM 层,实现对行的处理;
  3. 业务逻辑的处理放在 VM 中,同时放出需要其他层配合的回调(V 层或者 C 层)
  4. 有必要的情况下,可以进行双向绑定

C 层与 MVP 中的 C 层类似,只不过把 P 层换成 VM 层,并把变更了引用方式。

MVVM 示例

特别说明:为了更方便的使用 MVVM ,我导入了 RAC 框架

MVVM 登录.png

LoginView.h
//
//  LoginView.h
//  login
//

#import <UIKit/UIKit.h>
#import "LoginViewModel.h"

@interface LoginView : UIView
//MARK:- method
- (instancetype)initWithFrame:(CGRect)frame
                    ViewModel:(LoginViewModel *)viewModel;
@end

LoginView.m
//
//  LoginView.m
//  login
//

#import "LoginView.h"

@interface LoginView ()
/// 用户名输入框
@property (nonatomic, strong) UITextField *userNameTxt;
/// 密码输入框
@property (nonatomic, strong) UITextField *passWordTxt;
/// 登录按钮
@property (nonatomic, strong) UIButton *loginBtn;
/// login viewModel
@property (nonatomic, strong) LoginViewModel *viewModel;
@end

@implementation LoginView

- (instancetype)initWithFrame:(CGRect)frame
                    ViewModel:(LoginViewModel *)viewModel {
    if (self = [super initWithFrame:frame]) {
        self.viewModel = viewModel;
        
        [self createUI];
    }
    return self;
}

/**
 创建视图
 */
- (void)createUI {
    [self addSubview:self.userNameTxt];
    [self addSubview:self.passWordTxt];
    [self addSubview:self.loginBtn];
    
    [self bindViewModel];
}
//MARK:- VMBind
/**
 绑定VM
 */
- (void)bindViewModel {
    /// 建立 model -> view 的绑定
    [self.viewModel.userNameChange subscribeNext:^(id x) {
        self.userNameTxt.text = x;
    }];
    [self.viewModel.passWordChange subscribeNext:^(id x) {
        self.passWordTxt.text = x;
    }];
    
    /// 建立 view -> model 的绑定
    self.viewModel.userNameSignal = [self.userNameTxt rac_textSignal];
    self.viewModel.passWordSignal = [self.passWordTxt rac_textSignal];
    
    /// 登录点击事件
    [[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        [self endEditing:YES];
        [self.viewModel.loginSubject sendNext:nil];
    }];
}

//MARK:- getter/setter
- (UITextField *)userNameTxt {
    if (!_userNameTxt) {
        _userNameTxt = [UITextField new];
        // 设置用户名基本样式
        _userNameTxt.font = [UIFont systemFontOfSize:15];
        _userNameTxt.borderStyle = UITextBorderStyleRoundedRect;
        _userNameTxt.placeholder = @"请输入用户名";
    }
    return _userNameTxt;
}

- (UITextField *)passWordTxt {
    if (!_passWordTxt) {
        _passWordTxt = [UITextField new];
        // 设置密码基本样式
        _passWordTxt.secureTextEntry = YES;
        _passWordTxt.font = [UIFont systemFontOfSize:15];
        _passWordTxt.borderStyle = UITextBorderStyleRoundedRect;
        _passWordTxt.placeholder = @"请输入密码";
    }
    return _passWordTxt;
}

- (UIButton *)loginBtn {
    if (!_loginBtn) {
        _loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        // 设置按钮的基本样式
        [_loginBtn setTitle:@"登录" forState:UIControlStateNormal];
        _loginBtn.titleLabel.font = [UIFont systemFontOfSize:15];
        [_loginBtn setBackgroundColor:[UIColor redColor]];
    }
    return _loginBtn;
}

@end

LoginViewModel.h
//
//  LoginViewModel.h
//  login
//

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

@class LoginModel;
@interface LoginViewModel : NSObject
/// model 用户名改变
@property (nonatomic, strong) RACSubject  *userNameChange;

/// model 密码改变
@property (nonatomic, strong) RACSubject  *passWordChange;

/// 用户名改变
@property (nonatomic, strong) RACSignal  *userNameSignal;

/// 密码改变
@property (nonatomic, strong) RACSignal  *passWordSignal;

/// 登录事件
@property (nonatomic, strong) RACSubject  *loginSubject;

/// 登录结果
@property (nonatomic, strong) RACSubject  *loginResultSubject;

#pragma mark- method

/**
 初始化

 @param loginModel 登录 model
 */
- (instancetype)initWithModel:(LoginModel *)loginModel;
@end

LoginViewModel.m
//
//  LoginViewModel.m
//  login
//

#import "LoginViewModel.h"

#import "LoginModel.h"

@interface LoginViewModel ()
/// 登录 model
@property (nonatomic, strong) LoginModel  *loginModel;
@end

@implementation LoginViewModel

- (instancetype)initWithModel:(LoginModel *)loginModel {
    if (self = [super init]) {
        self.loginModel = loginModel;
        
        [RACObserve(self.loginModel, userName) subscribeNext:^(id x) {
            [self.userNameChange sendNext:x];
        }];
        
        [RACObserve(self.loginModel, passWord) subscribeNext:^(id x) {
            [self.passWordChange sendNext:x];
        }];

    }
    return self;
}

//MARK:- netWork
- (void)loginRequest {
    // 进行网络请求 \
    成功保存账号密码 \
    失败弹框提示
    if (/* success && */self.loginResultSubject) {
        [self.loginResultSubject sendNext:@(YES)];
    }
    else if (/* fail && */self.loginResultSubject) {
        [self.loginResultSubject sendNext:@(NO)];
    }
}

//MARK:- event response
- (void)check {
    if ([self passWordCheck] && [self userNameCheck]) {
        // 没问题则进行网络请求
        [self loginRequest];
    }
    else {
        // 问题处理、提示
    }
}

- (BOOL)passWordCheck {
    // 同时包含大小写英文与数字且长度在8-16之间
    NSString *regex = @"^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    return [predicate evaluateWithObject:self.loginModel.passWord];
}

- (BOOL)userNameCheck {
    // 包含大小写英文与数字且长度在6-20之间
    NSString *regex = @"^[a-zA-Z0-9]{8,16}$";
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];;
    
    return [predicate evaluateWithObject:self.loginModel.userName];
}


#pragma mark- getter/setter
- (RACSubject *)loginSubject {
    if (!_loginSubject) {
        _loginSubject = [RACSubject subject];
    }
    return _loginSubject;
}

- (RACSubject *)userNameChange {
    if (!_userNameChange) {
        _userNameChange = [RACSubject subject];
    }
    return _userNameChange;
}

- (RACSubject *)passWordChange {
    if (!_passWordChange) {
        _passWordChange = [RACSubject subject];
    }
    return _passWordChange;
}

- (RACSubject *)loginResultSubject {
    if (!_loginResultSubject) {
        _loginResultSubject = [RACSubject subject];
    }
    return _loginResultSubject;
}

- (void)setUserNameSignal:(RACSignal *)userNameSignal {
    _userNameSignal = userNameSignal;
    [self.userNameSignal subscribeNext:^(id x) {
        self.loginModel.userName = x;
    }];
}

- (void)setPassWordSignal:(RACSignal *)passWordSignal {
    _passWordSignal = passWordSignal;
    [self.passWordSignal subscribeNext:^(id x) {
        self.loginModel.passWord = x;
    }];
}

@end

LoginViewCOntroller.m
//
//  LoginViewCOntroller.m
//  login
//

#import "LoginViewCOntroller.h"

#import "LoginView.h"

#import "LoginModel.h"

#import "LoginViewModel.h"

@interface LoginViewCOntroller ()
/// 登录视图
@property (nonatomic, strong) LoginView *loginView;
/// 登录模型
@property (nonatomic, strong) LoginModel *loginModel;
/// 登录 viewModel
@property (nonatomic, strong) LoginViewModel *loginViewModel;

@end

@implementation LoginViewCOntroller

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createUI];
}

/**
 创建视图
 */
- (void)createUI {
    /// 这里的布局由于是 demo,所以是写死的,没有进行过适配,子页面中的也是一样
    [self.view addSubview:self.loginView];
}

//MARK:- private method

/**
 登录成功
 */
- (void)loginSuccess {
    
}

/**
 登录失败
 */
- (void)loginFail {
    
}

//MARK:- getter/setter
- (LoginView *)loginView {
    if (!_loginView) {
        _loginView = [[LoginView alloc] initWithFrame:CGRectMake(20, 60, self.view.frame.size.width-40, 160) ViewModel:self.loginViewModel];
    }
    return _loginView;
}

- (LoginViewModel *)loginViewModel {
    if (!_loginViewModel) {
        _loginViewModel = [[LoginViewModel alloc] initWithModel:self.loginModel];
        
        @weakify(self);
        [_loginViewModel.loginResultSubject subscribeNext:^(id x) {
            @strongify(self);
            if ([x boolValue]) {
                [self loginSuccess];
            }
            else {
                [self loginFail];
            }
        }];
    }
    return _loginViewModel;
}

- (LoginModel *)loginModel {
    if (!_loginModel) {
        _loginModel = [LoginModel new];
    }
    return _loginModel;
}
@end

MVVM 跟 MVP 相比,更加的独立,只负责逻辑,可以进行专门的开发和处理,不需要视图的配合,同时具有可重用的能力,可以让多个视图共同使用这段逻辑,同时允许进行数据的绑定和监听。简单说来就是 MVP 该有的优点都有,同时更上一层楼,但是 MVP 的多文件缺点也同时保留下来了。


结尾

MVX 系列的设计模式就暂且到此,查看代码与类之间的结构能够更方便的了解文章中所想表达的内容。当然,这篇文章属于是一篇漫谈,也就是表述我自身所了解的、所理解的 MVX 设计模式,有不足和错误的地方希望能够指正。


猜你喜欢

转载自juejin.im/post/7115676793578717214