objc-io 学习笔记 MVVM

MVVM 全称 Model-View-ViewModel,是 MVC 框架的增强版,它能减少 View Controller 的复杂性并使得表示逻辑更易于测试。

传统 MVC 框架如图:

image

Model 呈现数据,View 呈现用户界面,而 View Controller 调节它两者之间的交互。

MVVM框架如图:

image

View 和 View Controller 成对出现,成为一个整体,View Model 位于 View/Controller 与 Model 之间。

为什么这样设计?

在典型的 MVC 应用里,许多逻辑被放在 View Controller 里。它们中的一些确实属于 View Controller,但更多的是所谓的“表示逻辑(presentation logic)”,以 MVVM 属术语来说——就是那些从 Model 转换数据为 View 可以呈现的东西的事情,例如将一个 NSDate 转换为一个格式化过的 NSString。将表示逻辑从 Controller 移出放到一个新的对象里,即 View Model。这样就可以有效减小 ViewController 的复杂度并让架构看起来更清晰。

几个关键点:

  • MVVM 兼容你当下使用的 MVC 架构,如果当前你的工程还是用的 MVC,那么赶紧重构一下吧。
  • MVVM 增强应用的可测试性,不需要依赖于 View 和 ViewController,只通过 Model 和 ViewModel 就可以进行测试。
  • MVVM 配合一个绑定机制效果最好,比如 KVO。

下面进行一下代码对比,加深理解:

传统 MVC

1
2
3
4
5
6
7
8
9
10
@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

在 viewDidLoad 里,只需要基于它的 model 属性设置一些 Label 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];

if (self.model.salutation.length > 0) {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
} else {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
}

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}

现在来看看我们如何用一个 View Model 来增强它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//声明
@interface PersonViewModel : NSObject

- (instancetype)initWithPerson:(Person *)person;

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;

@end

//实现
@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
self = [super init];
if (!self) return nil;

_person = person;
if (person.salutation.length > 0) {
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
} else {
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
}

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
_birthdateText = [dateFormatter stringFromDate:person.birthdate];

return self;
}

@end

我们已经将 viewDidLoad 中的表示逻辑放入我们的 View Model 里了。此时,我们新的 viewDidLoad 就会非常轻量:

1
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}

注意到在这个简单的例子中, Model 是不可变的,所以我们可以只在初始化的时候指定我们 View Model 的属性。对于可变 Model,我们还需要使用一些绑定机制,这样 View Model 就能在背后的 Model 改变时更新自身的属性。此外,一旦 View Model 上的 Model 发生改变,那 View 的属性也需要更新。Model 的改变应该级联向下通过 View Model 进入 View。在 OS X 上,我们可以使用 Cocoa 绑定,但在 iOS 上我们并没有这样好的配置可用。我们想到了 KVO(Key-Value Observation),不过 Cocoa 原生 KVO 有很多让人诟病的地方,我推荐两个更好的方案:

  • ReactiveCocoa 这里有个 例子,对学习 MVVM 以及 ReactiveCocoa 都很有帮助,同时推荐 一本书
  • KVOController这里 附有一个例子,在这个例子中需要注意一点的是对于 NSMutableArray 类型的变量,如何保证其在增加或者删除元素的时候通知到观察者呢?对,通过 - (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; 该方法在 NSKeyValueCoding.h 中声明
1
2
3
4
5
@interface NSObject(NSKeyValueCoding)
...
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
...

@end

该方法返回 keyPath 对应的一个代理对象,通过这个代理对象,就可以在增加/删除元素的时候通知到观察者。这篇文章也有描述。

参考资料:

https://github.com/nixzhu/dev-blog/blob/master/2014-06-10-mvvm.md

http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/

http://cocoasamurai.blogspot.ca/2013/03/basic-mvvm-with-reactivecocoa.html

https://medium.com/@ramshandilya/lets-discuss-mvvm-for-ios-a7960c2f04c7