Welkin's Secret Garden

Pratice of View Model

I have recently finished serveral refactoring jobs that inserting a new layer called view model , and just find it delightful. There are so many ways to work with view model but I will find the most suitable way within this page.

First I will show the diagrams of MVC and MVVM.

MVC
MVC

MVVM
MVVM

I am not going to explain what view model and MVVM are. For more introduction, find here. So let’s start up.

Delegate and Block

We know that we have some ways to get the wanted data after receiving the response from server or database, and the most popular ways are delegate and block . Just have a glimpse of an example.

The yellow view should be placed an avatar and the below is the label to show username. We can easily write down two ways to handle the response in the view controller.

delegate

1
2
3
4
5
6
7
8
9
10
_viewModel = [[ViewModel alloc] init];
_viewModel.delegate = self;
[_viewModel request];

#pragma mark - ViewModelDelegate
- (void)viewModel:(ViewModel *)viewModel didFetchAvatarUrl:(NSString *)avatarUrl name:(NSString *)name
{
_headerView.avatarUrl = avatarUrl;
_headerView.name = name;
}

block

1
2
3
4
5
6
7
_viewModel = [[ViewModel alloc] init];
__weak typeof(self) weakSelf = self;
[_viewModel requestWithCompletion:^(NSString *avatarUrl, NSString *name) {
typeof(self) strongSelf = weakSelf;
strongSelf.headerView.avatarUrl = avatarUrl;
strongSelf.headerView.name = name;
}];

This is not the key point and everyone has her/his view about which way to choose. However I strongly recommend the first one because it is well-documented . What kind of the data is and how many methods will be called are coded clearly, and you can find everything in the protocol. Meanwhile, you can just call -[ViewModel request] anywhere and you don’t need to write the callbacks again and again, so you merely need to handle the only one entrance of the wanted data at the delegate methods. Of course, you even don’t need to worry about the appearance of retain-cycle .

View Retained View Model

With the help of view model and delegate, things seem good now, but what if there are something more? Let’s add another view to view controller.

Now, there is another table view on it. How shall we handle this? The easily way is to add another request method in the view model and declare another callback method in the protocol like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@protocol ViewModelDelegate <NSObject>

- (void)viewModel:(ViewModel *)viewModel didFetchAvatarUrl:(NSString *)avatarUrl name:(NSString *)name;
- (void)viewModel:(ViewModel *)viewModel didFetchTableViewData:(NSArray *)data;

@end

@interface ViewModel : NSObject

@property (nonatomic, weak) id<ViewModelDelegate> delegate;

- (void)requestHeaderViewData;
- (void)requestTableViewData;

@end

This seems to fit the change. But what if there are even more views in a view controller? It’s known that after more and more logic and functions being added into a view controller, the view model will become fatter and fatter and the view controller will still turn to huge because of the delegate’s callbacks.

Back to the diagrams of MVVM above, we learn that view controller is now regarded as a view . So why not put the view model into each subview so that each view can take over it’s own view model?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface HeaderView () <ViewModelDelegate>

@property (nonatomic, strong) ViewModel *viewModel;

@end

@implementation HeaderView

- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
... // init UI

_viewModel = [[ViewModel alloc] init];
_viewModel.delegate = self;
[_viewModel request];
}
return self;
}

@end

Just like this. After Initializing itself, the view initializes it’s view model and starts to fetch the data it wants and receives the response within this file. The exciting thing is the view controller returns to thin. No matter how many views are placed on it and how much logic is added, the only thing it need to do is simply create the views and then neglects the rest.

Component

As we believe that everything works well now, unexpected situation occurs. If there is some logic which must be run before the view is created , it is impossible for the solution above to deal with. In addition, there is a potential problem that if the view model logic need to be modified one day, the view may have to be modified too because of the strong association between view and view model.

In this case, I have try inserting another layer between view and view model.

I will show you the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface HeaderComponent : NSObject

@property (nonatomic, weak) UIView *superView;
@property (nonatomic, assign) CGRect frame;

@end

@interface HeaderComponent () <ViewModelDelegate>

@property (nonatomic, strong) ViewModel *viewModel;
@property (nonatomic, strong) HeaderView *headerView;

@end

This is enough to declare what the new layer is. The layer named component is actually a controller , taking charge of the job that view controller used to do. I rename it to component in order to avoid the confusion of view controller and it.

After adding the component layer, the problems are solved. View controller doesn’t need to know anything about the view model and even the view!

The component can be a black box . The view may even be shown or dismiss at any time without the help of view controller. The view controller can get message from the component only when it actually wants to know. What the most pleasant thing is the view model and the view can be replaced although the logic will be changed in the future and the whole component can also be reused.