对于MVVM框架,大家应该并不陌生,如果对这方面还不清楚的,可以去看一下一下三篇文章,应该会有一个比较清楚的认识。
MVVM奇葩说
被误解的 MVC 和被神化的 MVVM
iOS 架构模式–解密 MVC,MVP,MVVM以及VIPER架构
读了这三篇文章,你应该就不会对MVVM陌生了, 我这里算是对以上几篇文章以及个人的理解,上代码展示一下自己认为的MVVM写法,当然:我这里的写法是从唐巧的猿题库里面借鉴过来的,算是对MVVM的一个变种吧。
Talk is cheap, show you the code.
1、M层
1 | #import <Foundation/Foundation.h> |
2、V层
当然,严格上说Controller也是V层,但我比较喜欢把Controller看成是“胶水”,也就是把M、V、VM链接在一起然后展示到界面的强力胶,所以这里的V层主要展示SystemMessageCell。
1 | #import <UIKit/UIKit.h> |
SystemMessageCell.m文件,其实也就是大家常写的控件的创建(单纯的创建,不写任何业务逻辑,最后赋值还是用setFrameModel进行赋值)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@implementation JBSystemMessageCell
+ (instancetype)cellWithTableView:(UITableView *)tableView {
staticNSString *reuseID = @"JBSystemMessageCell";
JBSystemMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseID];
if (!cell) {
cell = [[JBSystemMessageCell alloc] initWithStyle:UITableViewCellStyleDefaultreuseIdentifier:reuseID];
}
return cell;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [superinitWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.backgroundColor = BackgroundColor;
// 点击cell的时候不要变色
self.selectionStyle = UITableViewCellSelectionStyleNone;
// 设置标题cell
[self setUpCell];
}
return self;
}
赋值:setFramModel,当然你也可以像猿题库里面那样自己写一个方法进行赋值都是可以的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- (void)setFrameModel:(JBSystemMessageFrameModel *)frameModel {
_frameModel = frameModel;
self.container.frame = frameModel.containerFrame;
// 标题
self.titleLabel.text = frameModel.messageModel.title;
self.titleLabel.frame = frameModel.titleLabelFrame;
// 时间-作者
self.timeLabel.frame = frameModel.timeLabelFrame;
// 内容
self.contentLabel.text = frameModel.messageModel.content;
self.contentLabel.frame = frameModel.contentLabelFrame;
// 查看详情
self.moreView.frame = frameModel.moreViewFrame;
}
- (void)moreInformation {
if (self.delegate && [self.delegate respondsToSelector:@selector(moreInformation:)]) {
[self.delegate moreInformation:self.frameModel];
}
}
3.VM层
VM层即ViewModel,就是处理API获取的数据转化成界面展示的模型数据。
1 | // 控件与cell之前的顶部间距 |
在ViewModel的.m文件中,依然是利用重写setMessageModel进行控件的尺寸以及展示数据逻辑等计算和转换。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
35
36
37
38
39
40
41
42
43
44
45
46@implementation JBSystemMessageFrameModel
- (void)setMessageModel:(JBSystemMessageModel *)messageModel {
_messageModel = messageModel;
CGFloat cellWidth = ScreenWidth;
/** 标题 */
CGFloat titleX = kJBMessageCellLeftMargin;
CGFloat titleY = kJBMessageCellLeftMargin;
CGFloat titleH = kJBMessageTitleHeight;
CGSize titleSize = [messageModel.titlesizeWithfont:kJBMessageCellTitleFontmaxSize:CGSizeMake(MAXFLOAT, titleH)];
self.titleLabelFrame = (CGRect){{titleX, titleY}, titleSize};
/** 时间/作者 */
CGFloat timeX = kJBMessageCellLeftMargin;
CGFloat timeY = CGRectGetMaxY(self.titleLabelFrame) +kJBMessageTopMargin;
CGFloat timeH = kJBMessageTimeHeight;
CGSize timeSize = [messageModel.publishedTimesizeWithfont:kJBMessageCellTimeFontmaxSize:CGSizeMake(MAXFLOAT, timeH)];
CGSize authorSize = [messageModel.authorsizeWithfont:kJBMessageCellTimeFontmaxSize:CGSizeMake(MAXFLOAT, timeH)];
CGFloat timeLabelWidth = timeSize.width + kJBMessageTopMargin + authorSize.width;
self.timeLabelFrame = CGRectMake(timeX, timeY, timeLabelWidth, timeH);
/** 内容 */
CGFloat contentX = kJBMessageCellLeftMargin;
CGFloat contentY = CGRectGetMaxY(self.timeLabelFrame) + kJBMessageTopMargin;
CGSize contentSize = [messageModel.contentsizeWithfont:kJBMessageCellSourceFontmaxSize:CGSizeMake(cellWidth -kJBMessageCellLeftMargin * 4, MAXFLOAT)];
CGSize contentOneLineSize = [@"聚保"sizeWithfont:kJBMessageCellSourceFontmaxSize:CGSizeMake(cellWidth -kJBMessageCellLeftMargin * 4,MAXFLOAT)];
CGFloat contentHeight = self.isShowMore ? contentSize.height : contentOneLineSize.height;
self.contentLabelFrame = CGRectMake(contentX, contentY, contentSize.width, contentHeight);
/** 查看详情 */
CGFloat moreViewX = 0;
CGFloat moreViewY = CGRectGetMaxY(self.contentLabelFrame) + kJBMessageTopMargin;
CGFloat moreViewH = kJBMessageInfoHeight;
CGFloat moreViewW = cellWidth - kJBMessageCellLeftMargin * 2;
self.moreViewFrame = CGRectMake(moreViewX, moreViewY, moreViewW, moreViewH);
/** 容器 */
CGFloat contrainerX = kJBMessageCellLeftMargin;
CGFloat contrainerY = kJBMessageCellTopMargin;
CGFloat contrainerW = moreViewW;
CGFloat contrainerH = CGRectGetMaxY(self.moreViewFrame);
self.containerFrame = CGRectMake(contrainerX, contrainerY, contrainerW, contrainerH);
/** cell高度 */
self.cellHeight = contrainerY + contrainerH;
}
现在View部分的cell视图有了,Model模型和ViewModel展示数据模型都有了,Controller该怎么写呢?毕竟前面我说过,我认为Controller只是一个胶水而已,怎么才能不导致回到MVC(Massive Controller)呢?在这里我借鉴猿题库 iOS 客户端架构设计在中间引入一个DataSerVice来对Controller进行瘦身,并达到对每个模块解耦并可单独测试。
DataService部分的代码
1 | #import <Foundation/Foundation.h> |
这里要注意一下,对于给外部暴露的systemMessageArray这个数组最好在生命属性的时候加上readonly,因为dataService是专门处理数据的,数据不应该在其他任何外部地方被修改,做到各司其职。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
35
36
37@interface JBSystemMessageDataService ()
@property (nonatomic, strong, nonnull) NSMutableArray<JBSystemMessageFrameModel *> *systemMessageArray;
@end
@implementation JBSystemMessageDataService
- (NSMutableArray<JBSystemMessageFrameModel *> *)systemMessageArray {
if (!_systemMessageArray) {
_systemMessageArray = [NSMutableArrayarray];
}
return_systemMessageArray;
}
- (void)requestSystemMessageDataWithCallback:(JBCompletionCallback)callback {
JBWeakSelf;
[JBHttpTool get:JBSystemMessageInfoparameters:nilsuccess:^(id json) {
callback(...);
} failure:^(NSError *error) {
callback(...);
}];
}
- (void)updateModel:(JBSystemMessageFrameModel *_Nonnull)frameModel callback:(nonnullJBCompletionCallback)callback {
JBSystemMessageFrameModel *newFrameModel = [JBSystemMessageFrameModel new];
newFrameModel.isShowMore = frameModel.isShowMore;
newFrameModel.messageModel = frameModel.messageModel;
NSInteger index = [self.systemMessageArrayindexOfObject:frameModel];
[self.systemMessageArrayreplaceObjectAtIndex:index withObject:newFrameModel];
callback(@(index));
}
1 | requestSystemMessageDataWithCallback是请求API并对请求的结果进行回调。当然这里只是进行简单展示一下,你也可以进行自己的理解和设计把数据请求这一块单独做一个处理并供整个项目使用,这里就不再累述。 |
现在DataService部分已经进行简单展示了,Controller就很好处理:进行简单的胶水黏合作用。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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80#import "JBSystemMessageController.h"
#import "JBSystemMessageCell.h"
#import "JBSystemMessageDataService.h"
@interface JBSystemMessageController () <JBSystemMessageDelegate>
@property (nonatomic,strong, nullable) JBSystemMessageDataService *dataService;
@end
@implementation JBSystemMessageController
- (JBSystemMessageDataService *)dataService {
if (!_dataService) {
_dataService = [[JBSystemMessageDataServicealloc] init];
}
return_dataService;
}
- (void)viewDidLoad {
[superviewDidLoad];
[selfsetUpData];
[selfsetUpTableView];
}
- (void)setUpTableView {
self.tableView.backgroundColor =BackgroundColor;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
self.tableView.showsVerticalScrollIndicator = NO;
}
- (void)setUpData {
JBWeakSelf;
[self.dataServicerequestSystemMessageDataWithCallback:^(id _Nonnull callback) {
JBIsSuccess(callback) ? [weakSelf.tableViewreloadData] : [MBProgressHUDshowError:callback];
}];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataService.systemMessageArray.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.dataService.systemMessageArray[indexPath.row].cellHeight;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
JBSystemMessageCell *systemMessageCell = [JBSystemMessageCellcellWithTableView:tableView];
systemMessageCell.frameModel = self.dataService.systemMessageArray[indexPath.row];
systemMessageCell.delegate = self;
return systemMessageCell;
}
#pragma mark- cell点击代理
- (void)moreInformation:(JBSystemMessageFrameModel *)frameModel {
JBWeakSelf;
[self.dataServiceupdateModel:frameModel callback:^(id _Nonnull callback) {
// 模型在数组中的索引
NSInteger index = [callback integerValue];
[weakSelf.tableView beginUpdates];
[weakSelf.tableViewreloadRow:index inSection:0withRowAnimation:UITableViewRowAnimationAutomatic];
[weakSelf.tableView endUpdates];
[weakSelf.tableViewscrollToRow:index inSection:0atScrollPosition:UITableViewScrollPositionBottomanimated:YES];
}];
}
初始化数据,初始化tableView, 然后复制tableView数据源 并 走一下cell的代理方法就OK了~ 其他的controller根本不需要管,Model和ViewModel压根就跟Controller没有半毛钱的关系,头文件都不需要倒入,controller真正关心的只是View和DataService两个;dataService关心的只是ViewModel界面展示数据的处理。而ViewModel关心的只是Model层数据结构。这样进行设计架构,不仅仅对controller进行了瘦身,各个部分也进行了解耦,另外这么设计也有一个好处就是各个部分可以进行相应的复用,而且项目的维护(特别是新来的接手别人的“杰作”的时候,应该还算比较酸爽的,不至于像以前那样抱怨:这谁写的啊,看他代码我还不如删了自己重新写)起来也是比较方便的。
总结:
以上所展示的算是MVVM的一种改良吧,借鉴猿题库的架构思想。当然,每个人可能对MVVM都有自己的理解,可以根据自己的理解进行设计出合理的框架,我在这里只是做一个抛砖引玉,简单的展示下。大家可以自己发挥,如果有好的建议,可以留言进行探讨,另外理解错误的地方希望大家斧正~
这里稍微有点懒了,开年工作忙,所以贴了很多代码, 希望大家谅解。