iOS进阶 MVVM实践

1,140 阅读4分钟

最近看了一些MVVM的文章,总感觉说得很模糊,所以写出自己的看法。如果观点有出入,还望指正

MVVM架构图

截屏2022-01-11 下午9.45.54.png

先看文件结构划分

截屏2022-01-11 下午9.29.33.png

简单的交互页面展示

截屏2022-01-11 下午10.38.46.png

具体代码

  • User中 这个model很简单,就是个数据结构model,没有依赖项,完全解耦,可复用
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int height;

@end
  • UserView中 UserView只暴露了3个方法 1.更新视图的方法。 2.与外界交互的代理方法。UserView没有依赖任何其他项,是完全解耦的,所以可以做到完全复用,其他地方想要使用只要调用这两个方法即可
.h
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@protocol UserView_delegate <NSObject>
-(void) userViewAddHeight;
-(void) userViewJump;
@end

@interface UserView : UIView
//使用代理进行与ViewModel的交互
@property (nonatomic,weak) id<UserView_delegate> delegate;
//对外暴露更新视图的方法
- (void)updateViewWithName:(NSString *)name height:(int)height;
@end

NS_ASSUME_NONNULL_END


.m
#import "UserView.h"

@interface UserView()
@property (nonatomic, strong) UILabel *label_userInfo;
@property (nonatomic, strong) UIButton *button_addHeight;
@end

@implementation UserView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self addSubview:self.label_userInfo];
        [self addSubview:self.button_addHeight];
        [self addSubview:self.button_jump];
        self.backgroundColor = [UIColor systemPinkColor];
    }
    return self;
}

- (void)addHeight:(UIButton *)button {
    if (self.delegate && [self.delegate respondsToSelector: @selector(userViewAddHeight)]) {
        [self.delegate userViewAddHeight];
    }
}

- (void)jump:(UIButton *)button {
    if (self.delegate && [self.delegate respondsToSelector: @selector(userViewJump)]) {
        [self.delegate userViewJump];
    }
}

- (void)updateViewWithName:(NSString *)name height:(int)height {
    self.label_userInfo.text = [NSString stringWithFormat:@"name:%@  height:%dcm",name,height];
}

-(UILabel *)label_userInfo {
    if (!_label_userInfo) {
        _label_userInfo = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 50)];
        _label_userInfo.textColor = [UIColor blackColor];
        _label_userInfo.backgroundColor = [UIColor whiteColor];
    }
    return _label_userInfo;;
}

- (UIButton *)button_addHeight {
    if (!_button_addHeight) {
        _button_addHeight = [[UIButton alloc] initWithFrame:CGRectMake(0, 120,150, 50)];
        [_button_addHeight addTarget:self action:@selector(addHeight:) forControlEvents:(UIControlEventTouchUpInside)];
        _button_addHeight.backgroundColor = [UIColor yellowColor];
        [_button_addHeight setTitle:@"点击长高一公分" forState:(UIControlStateNormal)];
        [_button_addHeight setTitleColor:[UIColor blackColor] forState:(UIControlStateNormal)];
    }
    return _button_addHeight;
}

- (UIButton *)button_jump {
    if (!_button_jump) {
        _button_jump = [[UIButton alloc] initWithFrame:CGRectMake(0, 120,150, 50)];
        [_button_jump addTarget:self action:@selector(jump:) forControlEvents:(UIControlEventTouchUpInside)];
        _button_jump.backgroundColor = [UIColor yellowColor];
        [_button_jump setTitle:@"跳转到用户详情" forState:(UIControlStateNormal)];
        [_button_jump setTitleColor:[UIColor blackColor] forState:(UIControlStateNormal)];
    }
    return _button_jump;
}

@end
  • UserViewModel中
  1. UserViewModel中持有了UserView和处理User这个model的逻辑,减轻了原来ViewController的负担

  2. 通过kvo检测user的变化,实时更新视图

  3. 通过遵循UserView的代理方法,进行用户交互、处理数据

  4. 对外暴露是rUserViewreadOnly修饰的,确保在外部不被修改

  5. 如果逻辑有什么变化,比如点击一次增加2cm,将可以直接在UserViewMode中处理,即代理方法找修改成

-(void)userViewAddHeight {
    self.user.height = self.user.height + 2;
}

快速高效,且改动极小

.h
#import <Foundation/Foundation.h>
#import "UserView.h"
#import "User.h"

NS_ASSUME_NONNULL_BEGIN

@interface UerViewModel : NSObject<UserView_delegate>
@property (nonatomic, strong, readonly) UserView *rUserView;
- (void)requestUserData;
@end

NS_ASSUME_NONNULL_END

.m
#import "UerViewModel.h"

@interface UerViewModel()
@property (nonatomic, strong) User *user;
@property (nonatomic, strong) UserView *userView;

@end

@implementation UerViewModel
//处理网络请求
- (void)requestUserData {
    //处理model转化
    User *user = [User new];
    user.usedId = @"9527";
    user.height = 190;
    user.name = @"lee";
    //kvo会自动检测到变化,调用视图更新
    self.user = user;
}

- (instancetype)init {
    if (self = [super init]) {
        self.user = [User new];
        self.userView = [[UserView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 200)];
        self.userView.delegate = self; //记得添加代理
        //使用kvo检测model变化
        [self addObserver:self forKeyPath:@"user" options:(NSKeyValueObservingOptionNew) context:nil];
        [self addObserver:self forKeyPath:@"user.height" options:(NSKeyValueObservingOptionNew) context:nil];
    };
    
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString: @"user"] || [keyPath isEqualToString: @"user.height"]) {
        [self.userView updateViewWithName:self.user.name height:self.user.height];
    }
}

//对外是readOnly
- (UserView *)rUserView {
    return self.userView;
}

//代理方法
#pragma mark UserView_delegate
-(void)userViewAddHeight {
    //KVO会自动监测更新
    self.user.height++;
}

-(void)userViewJump {
    //与ViewController交互,这里使用block
    if (self.block) {
        self.block(self.user.usedId);
    }
}

-(void)dealloc {
    //记得移除观察者
    [self removeObserver:self forKeyPath:@"user"];
    [self removeObserver:self forKeyPath:@"user.height"];
}
@end
  • ViewController中 1.ViewController中代码极其简单,仅仅是持有了viewModel而已,还有就是必要的主动请求数据的方法

2.ViewModel 与 ViewConroller的交互使用block,当然也可以使用代理等其他方式

3.如果这个ViewController中还有其他的View,只需要增加新的viewModel即可

4.由于ViewController仅持有ViewModel, 没有做其他数据处理,所以整个文件很轻,也很好维护

.m 文件
#import "RFViewViewController.h"
#import "UerViewModel.h"
@interface RFViewViewController ()
@property (nonatomic,strong) UerViewModel *userViewModel;

@end

@implementation RFViewViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.userViewModel.rUserView];
    [self.userViewModel requestUserData];
}

- (UerViewModel *)userViewModel {
    if (!_userViewModel) {
        _userViewModel = [[UerViewModel alloc] init];
        //我这里是用block做UerViewModel 和 ViewViewController,当然也可以使用代理等其他方式
        _userViewModel.block = ^(NSString * _Nonnull userId) {
            使用MGJRouter 做路由跳转
            [MGJRouter openURL:@"MVVM://UserDetail"withUserInfo:@{
                @"navigationVC" : self.navigationController,
                @"userid": userId },
            }completion:nil];
        };
    }
    return _userViewModel;
}

总结

  1. Model和View之间是完全解耦的,这两块可以完全复用
  2. ViewModel持有了model和view,包含了网络请求和逻辑处理,又通过KVO做到视图实时更新; 如果有网络或者逻辑等方面的修改,只需要在ViewModel修改即可,影响面很小
  3. ViewController持有ViewModel,但是由于ViewModel并不依赖ViewController,所以ViewModel是完全解耦了,如果其他地方需要用到这ViewModel模块,需要搬运ViewController中的代码即可,复用度极高。
  4. 真实项目上肯定不止一个ViewModel,按照这种模式,完全可以多设置几个ViewModel。各个模块分工明确,且交互并不杂糅在一起