iOS MVVM+RAC 从框架到实战

原文

一、前言

很早之前就想写写自己在架构模式方面的心得,但是一直感觉自己是井底之蛙,毕竟在iOS领域越深入越感到自己的无知,心中有着敬畏之心,就更没有自信去写这个东西(你也可以理解是没时间(>﹏<),请原谅我的装逼,嘿嘿).

对于架构模式这个让人又爱又恨的玩意,说来其实简单,但一千个人眼中就有一千种哈姆雷特,说他千变万化确实是事实,而且当你深入其中的时候你真的会上瘾,并乐此不疲!
当然更希望有更多的大神来指点一下,让我自己也让大家有提升就够了,万分感谢!

二、谈谈MVVM和RAC

1、MVVM浅析

到这里我就默认你看过MVVM相关文章(毕竟相关文章已经可以用满天飞来形容了(≧≦)/啦啦啦!),仅仅简要谈谈我对其的理解。

MVC是构建iOS App的标准模式,是苹果推荐的一个用来组织代码的权威范式,市面上大部分App都是这样构建的,具体组建模式不细说,iOS入门者都比较了解(虽然不一定能完全去遵守),但其几个不能避免的问题却是很严重困扰开发者比如厚重的ViewController、遗失的网络逻辑(没有属于它的位置)、较差的可测试性等因此也就会有维护性较强、耦合性很低的一种新架构MVVM (MVC 引申出得新的架构)的流行。

MVVM虽然来自微软,但是不应该反对它,它正式规范了正式规范了视图和控制器紧耦合的性质,如下图:

ViewModel: 相比较
于MVC新引入的视图模型。是视图显示逻辑、验证逻辑、网络请求等代码存放的地方,唯一要注意的是,任何视图本身的引用都不应该放在VM中,换句话说就是VM中不要引入UIKit.h (对于image这个,也有人将其看做数据来处理,这就看个人想法了,并不影响整体的架构)。

这样,首先解决了VC臃肿的问题,将逻辑代码、网络请求等都写入了VM中,然后又由于VM中包含了所有的展示逻辑而且不会引用V,所以它是可以通过编程充分测试的。

so,就是这个样子的,6666!

2、RAC浅浅析

特别浅。。。本文重点是框架及实战及MVVM思想,RAC这玩意话说学习曲线较长,难以理解,不好上手,是因为之前学习的时候使用者、中文教程还比较少,所以学习运用起来比较费劲,(当时确实废了好大得劲,实力装逼一把 @%&$%& )但现在已经成熟的烂大街了,只要有心,好的教程一大把,能潜下心来看我写的水文的人,拿下RAC不在话下!

ReactiveCocoa 可以说是结合了函数式编程和响应式编程的框架,也可称其为函数响应式编程(FRP)框架,强调一点,RAC虽然最大的优点是提供了一个单一的、统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和KVO.但是不要简单的只是单纯的认为他仅仅就是减少代码复杂度,更好的配合MVVM而已,小伙子,这样你就小看它了。

它最大的与众不同是提供了一种新的写代码的思维,由于RAC将Cocoa中KVO、UIKit event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函数的事。

如果全工程都使用RAC来实现,对于同一个业务逻辑终于可以在同一块代码里完成了,将UI事件,逻辑处理,文件或数据库操作,异步网络请求,UI结果显示,这一大套统统用函数式编程的思路嵌套起来,进入页面时搭建好这所有的关系,用户点击后妥妥的等着这一套联系一个个的按期望的逻辑和次序触发,最后显示给用户。

额,就说这么多,再说就没头了(≧≦)/啦啦啦!

3、本篇对两者的理解运用

在此次介绍中,会使用MVVM+RAC结合的方式,搞定一个添加上拉加载及下拉刷新的列表,所以更多的诠释MVVM思想,而不是RAC的逻辑链式操作(这一点用登录界面来写更能体现YoY ),RAC在此扮演的更大一部分的角色是更好的解耦,减少代码复杂度,使代码层次分明、逻辑清晰更便于维护升级。

三、框架部分

1、框架目录详解

首先介绍一下本框架的目录结构,如下图

1686195-eaa3247b76c7da54.jpeg

1)Frameworks

存放系统库的虚拟文件夹, 目前搭建框架的时候需要手动添加一个名称为Frameworks的虚拟文件夹,这样你在Build Phases 中添加的系统库会自动归入此文件夹,不会直接在外部显示以至于打乱目录结构。系统库添加流程如下:

1686195-9083083f67063979.jpg

另外,细心地家伙会发现此目录中有两个相同的Frameworks, 那这到底是什么鬼?最上面的那个Frameworks是在自己搭框架自己添加的,当时的项目还很单纯, 没有这么淘气,问题出在下面那个Pods Target上,添加它之后就会自动给你生成一个虚拟的Frameworks的文件夹,那又该问了为啥不直接用下面那个呢???(废话真多!反正也没冲突,就留着吧╮(╯﹏╰)╭)

既然提到了Pods,那接下来讲讲CocoaPods(第三方类库管理工具)。

2)CocoaPods

当你开发iOS应用时,会经常使用到很多第三方开源类库,比如JSONKit,AFNetWorking等等。可能某个类库又用到其他类库,所以要使用它,必须得另外下载其他类库,而其他类库又用到其他类库,“子子孙孙无穷尽也”,反正在早期我是体会过这种痛苦,好心酸,手动一个个去下载所需类库是十分麻烦的。

还有另外一种常见情况是,你项目中用到的类库有更新,你必须得重新下载新版本,重新加入到项目中,十分麻烦。

CocoaPods就是帮你解决上面的问题的,话说这玩意应该是iOS最常用最有名的类库管理工具了,作为iOS程序员的我们,掌握CocoaPods的使用是必不可少的基本技能了,至于这玩意该咋用?

O(∩_∩)O哈哈~你觉得我会告诉你么?好吧,我这人还是很心软的,下面一张图告诉你该咋用...

1686195-805a1ff43232609c.jpg

哎呦,不错哦~是不是get了一个新技能 ?6666!

3)AppDelegate

这个目录下放的是AppDelegate.h(.m)文件,是整个应用的入口文件,所以单独拿出来。一会儿告诉你如何写一个简洁的AppDelegate,会在这个文件夹里添加一些类,所以将其放入一个文件夹内还是很有必要的。

4)Class

工程主体类, 日常大部分开发代码均在这里,又细分了好多次级目录。

通用类

  • General : 通用类(文件夹项目移植过程中都不需要更改的就能直接使用的)

  • Base : 基类 (整个框架的基类)

  • Categories : 公共扩展类 (就是一些常用的类别,比如分享啊什么的)

  • Core : 公共核心类(一般存放个人信息、接口API等)

  • Models : 公共Model (公用的一些数据模型)

  • Views : 公共View (封装的一些常用的View)

工具类

  • Helpers : 工程的相关辅助类(比如类似数据请求、表单上传、网络监测等工具类)

宏定义类

  • Macro : 宏定义类 (就是整个应用会用到的宏定义)

  • AppMacro.h app项目的相关宏定义

  • NotificationMacro.h 通知相关的宏定义

  • VendorMacro.h 第三方相关宏定义

  • UtilsMacro.h 为简化代码的宏定义

  • ...等等等等(其他随你定啦!YoY )

APP具体模块代码类

  • Sections : 各模块的文件夹(一般而言,我们以人为单位)

  • LSSections 王隆帅的文件夹

  • CLSections 马成麟的文件夹

  • ...等等等等(也可以写你最喜欢的苍老师的,叼叼的!)

每个成员的文件夹下是其所负责模块的文件夹,比如苍老师负责PHP界面模块(我也认为PHP是最好的语言!大家可以在评论区谈论一下!),如下(接着上面的个人文件夹):

  • PHP : 模块名,也可以是首页(HomePage)...等等

  • ViewControllers 界面控制器存放处(这是文件夹名)

  • ViewModels 打杂的(MVVM的核心、解耦合、处理逻辑等)

  • Views 界面相关View存放处(界面相关子View)

  • Models 数据模型存放处(各种单纯的数据模型,一点都不胖,是标准的瘦Model)

这就是标准的MVVM了。。。为啥不和上面目录连起来呢?为啥呢?为啥呢?因为臣妾做不到啊!!!(不会三级、四级列表的MarkDown写法,求大神支招!良辰必有重谢!)

第三方类库

  • Vendors : 第三方的类库/SDK,如UMeng、WeiboSDK、WeixinSDK等等。

到这哥们又该疑惑了,心里该碎碎念了:What are you 弄啥嘞!刚才刚讲了个第三方库管理CocoaPods,你丫这里自己又搞了一个,信不信我突突了你!

哈哈哈,刚才的CocoaPods确实管理着大部分的第三方库,这里建立第三方库目录的原因有两个:其一,并不是所有的你需要的第三方都支持pods的,所以还是需要手动添加一些类库。其二,一些第三方库虽然支持pods,但是需要我们去更改甚至自定义这个第三方,此时也需要放入这里,也防止使用pods一不小心更新掉你的自定义!:-D你来打我啊!

5)Resource

这里放置的是工程所需的一些资源,如下

  • Fonts 字体

  • Images 图片(当然你可以添加至Assets.xcassets, 没人拦着你)

  • Sounds 声音

  • Videos 视频

ok,目录就讲到这里!想知道更详细的可以私信我!

2、基类详解

这里着重讲解一下VC、V、VM的基类,其他的模式与View类似所以略过,其中TableViewCell的基类稍微特殊所以也提一下。

我目前的基类如下图:

1686195-8238b46d0dcf925d.jpeg

是不是眼花缭乱了..., 我曾经也看它不顺眼, 曾经尝试过把基类都干掉,然后遇到了一些麻烦...就妥协了,在文章的最后可以跟大家聊聊我是怎么去干掉基类,然后又失败的,这里先详细讲一下基类。

1)YDViewController

1686195-b793d1be067dc270.jpg
1686195-44de2525b64a9e5a.jpg

函数的具体用意图已经标的很清楚了,这里简单讲一下四个函数的作用

  • yd_addSubviews : 添加View到ViewController

  • yd_bindViewModel : 用来绑定V(VC)与VM

  • yd_layoutNavigation : 设置导航栏、分栏

  • yd_getNewData : 初次获取数据的时候调用(不是特别必要)

2)YDView

1.jpg
2.jpg
  • yd_setupViews : 添加子View到主View

  • yd_bindViewModel : 绑定V与VM

  • yd_addReturnKeyBoard : 设置点击空白键盘回收

3)YDViewModel

3.jpg
4.jpg
  • yc_initialize : 进行一些逻辑绑定,网络数据请求处理。

  • LSRefreshDataStatus 数据处理后需要进行的操作标识

  • LSHeaderRefresh_HasMoreData 下拉还有更多数据

  • LSHeaderRefresh_HasNoMoreData 下拉没有更多数据

  • LSFooterRefresh_HasMoreData 上拉还有更多数据

  • LSFooterRefresh_HasNoMoreData 上拉没有更多数据

  • LSRefreshError 刷新出错

  • LSRefreshUI 仅仅刷新UI布局

4)YDTableViewCell

5.jpg

由于Cell比较特殊,所以单拎出来说一下。观察上面的ViewMdoel、View等的基类会发现每个基类都会有数据绑定的地方,但是cell得数据绑定需要放在数据初始化的时候,因为所有的基类的数据逻辑绑定都是在没有返回初始化对象的时候调用的,但是cell中假如在那里面进行数据绑定会出现问题比如下图:

上图中的函数假如是在 bindViewModel 内,则会复用失败,点击按钮是没有反应的,但是假如是在数据初始化的时候调用:比如 setViewModel 的时候,就会OK了,因为里面用到了cell的在RAC中复用机制 rac_prepareForReuseSignal ,在cell还没有初始化返回的时候是失效的。

3、题外话

基类的作用是统一管理,统一风格,便于编码,有更多的额外的附加功能的话,建议使用Protocol 或 Category,这样移植性强,便于管理与扩展,不至于牵一发而动全身。

本篇基类核心是用VM来配置V(VC),并提供一些必须的Protocol方法来处理界面显示、逻辑,将代码风格规范化,各个部分的功能明朗化,这样,当你需要写什么,需要找什么,需要更改什么的时候都会很明确这些代码的位置,逻辑更清晰,而不会浪费更多的时间在思考应该写在哪,该去哪找,要改的地方在哪这种不该费时间的问题上。

三、实战部分(经典列表的实现)

这里讲一下如下界面的代码构造方式,很普通的一个列表:(懒得再写了,这是我之前做的一个项目的一个界面,之前基类讲解中会看到都是YD开头的,在这里是YC开头就这个区别而已)

1686195-4cb3966fac6bc933.jpg

首先观察这个界面,需求是:头部的内容数量多的话是可以左右滑动的,然后整体是可以上拉加载的。我是这样处理的:首先界面整体是一个TableView,然后分为一个Header、一个Section和主体列表Row。在Header上嵌套一个CollectionView保证可复用。具体分层如下

1686195-b2fa505d3c2f3560.jpg

然后处理完后的目录如下:

23.jpg

简单介绍一下:

ViewController

  • LSCircleListViewController : 界面主控制器,负责跳转、Navgation、TabBar等

View

  • LSCircleListView : 界面主View,负责主要界面的显示

  • LSCircleListHeaderView : 头部Header,封装的内部含有一个CollectionView

  • LSCircleListCollectionCell : 头部Header中的CollectionView自定义的Cell

  • LSCircleListSectionHeaderView : SectionView,此界面不需复用,所以单纯一个View即可,若需要复用需要TableViewHeaderFooterView

  • LSCircleListTableCell : 主TableView的Cell

ViewModel

  • LSCircleListViewModel : 界面主ViewModel

  • LSCircleListHeaderViewModel : 头部Header对应的ViewModel

  • LSCircleListCollectionCellViewModel : 头部CollectionCell及TableViewCell的ViewModel(因为二者的数据结构是一致的)

  • LSCircleListSectionHeaderViewModel : Section的ViewModel

Model

  • LSCircleListModel : 圈子的数据模型(header和tableViewCell数据结构是一致的)

一个小小的界面这么多类...是不是难以接受了,淡定些,骚年!你要想想把这些个东西都放在VC内是个什么赶脚?也得好几千行呢!(有点夸张!不过也够头疼的),这么多类,这里着重讲一下主VC、主V、主VM、主M就ok,能详细讲明白MVVM之间是如何工作的就一通百通了。

1、LSCircleListViewController的处理

先上代码:

| 

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

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

//  LSCircleListViewController.m

//  ZhongShui

//  Created by 王隆帅 on 16/3/10.`

//  Copyright ? 2016年 王隆帅. All rights reserved.`



#import "LSCircleListViewController.h"`

`#import "LSCircleListView.h"`

`#import "LSCircleListViewModel.h"`

`#import "LSCircleMainPageViewController.h"`

`#import "LSCircleMainPageViewModel.h"`

`#import "LSCircleListCollectionCellViewModel.h"`

`#import "LSNewCircleListViewController.h"`

`@interface LSCircleListViewController ()`

`@property (nonatomic, strong) LSCircleListView *mainView;`

`@property (nonatomic, strong) LSCircleListViewModel *viewModel;`

`@end`

`@implementation LSCircleListViewController`

`- (void)viewDidLoad {`

`[``super` `viewDidLoad];`

`// Do any additional setup after loading the view.`

`}`

`#pragma mark - system`

`- (void)updateViewConstraints {`

`WS(weakSelf)`

`[self.mainView mas_makeConstraints:^(MASConstraintMaker *make) {`

`make.edges.equalTo(weakSelf.view);`

`}];`

`[``super` `updateViewConstraints];`

`}`

`#pragma mark - private`

`- (void)yc_addSubviews {`

`[self.view addSubview:self.mainView];`

`}`

`- (void)yc_bindViewModel {`

`@weakify(self);`

`[[self.viewModel.cellClickSubject takeUntil:self.rac_willDeallocSignal] subscribeNext:^(LSCircleListCollectionCellViewModel *viewModel) {`

`@strongify(self);`

`LSCircleMainPageViewModel *mainViewModel = [[LSCircleMainPageViewModel alloc] init];`

`mainViewModel.headerViewModel.circleId = viewModel.idStr;`

`mainViewModel.headerViewModel.headerImageStr = viewModel.headerImageStr;`

`mainViewModel.headerViewModel.title = viewModel.name;`

`mainViewModel.headerViewModel.numStr = viewModel.peopleNum;`

`LSCircleMainPageViewController *circleMainVC = [[LSCircleMainPageViewController alloc] initWithViewModel:mainViewModel];`

`[self.rdv_tabBarController setTabBarHidden:YES animated:YES];`

`[self.navigationController pushViewController:circleMainVC animated:YES];`

`}];`

`[self.viewModel.listHeaderViewModel.addNewSubject subscribeNext:^(id x) {`

`@strongify(self);`

`LSNewCircleListViewController *newCircleListVC = [[LSNewCircleListViewController alloc] init];`

`[self.rdv_tabBarController setTabBarHidden:YES animated:YES];`

`[self.navigationController pushViewController:newCircleListVC animated:YES];`

`}];`

`}`

`- (void)yc_layoutNavigation {`

`self.title = @``"圈子列表"``;`

`[self.rdv_tabBarController setTabBarHidden:NO animated:YES];`

`}`

`#pragma mark - layzLoad`

`- (LSCircleListView *)mainView {`

`if` `(!_mainView) {`

`_mainView = [[LSCircleListView alloc] initWithViewModel:self.viewModel];`

`}`

`return` `_mainView;`

`}`

`- (LSCircleListViewModel *)viewModel {`

`if` `(!_viewModel) {`

`_viewModel = [[LSCircleListViewModel alloc] init];`

`}`

`return` `_viewModel;`

`}`

`- (void)didReceiveMemoryWarning {`

`[``super` `didReceiveMemoryWarning];`

`// Dispose of any resources that can be recreated.`

`}`

`/*`

`#pragma mark - Navigation`

`// In a storyboard-based application, you will often want to do a little preparation before navigation`

`- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {`

`// Get the new view controller using [segue destinationViewController].`

`// Pass the selected object to the new view controller.`

`}`

`*/`

`@end`

对于VC,分为三个模块,下面分别来说一下:

i 第一个模块:系统函数

9.jpg

此函数是从iOS6.0开始在ViewController中新增一个更新约束布局的方法,这个方法默认的实现是调用对应View的 updateConstraints 。ViewController的View在更新视图布局时,会先调用ViewController的updateViewConstraints 方法。我们可以通过重写这个方法去更新当前View的内部布局,而不用再继承这个View去重写-updateConstraints方法。我们在重写这个方法时,务必要调用 super 或者 调用当前View的 -updateConstraints 方法。

ⅱ 第二个模块 : 私有函数

10.jpg

前面基类内也提到了这三个函数的具体作用,即

  • yd_addSubviews : 添加View到ViewController

  • yd_bindViewModel : 这里绑定了两个跳转事件。

  • yd_layoutNavigation : 设置了标题为“圈子列表”、及TabBar不隐藏

ⅲ 第三个模块 : 懒加载

11.jpg

这就不用解释了,用到时再加载。

2、View的处理

先上代码

| 

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

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

//  LSCircleListView.m`

//  ZhongShui`


//  Created by 王隆帅 on 16/3/10.`

`//  Copyright ? 2016年 王隆帅. All rights reserved.

`#import "LSCircleListView.h"`

`#import "LSCircleListViewModel.h"`

`#import "LSCircleListHeaderView.h"`

`#import "LSCircleListSectionHeaderView.h"`

`#import "LSCircleListTableCell.h"`

`@interface LSCircleListView ()  `

`@property (strong, nonatomic) LSCircleListViewModel *viewModel;`

`@property (strong, nonatomic) UITableView *mainTableView;`

`@property (strong, nonatomic) LSCircleListHeaderView *listHeaderView;`

`@property (strong, nonatomic) LSCircleListSectionHeaderView *sectionHeaderView;`

`@end`

`@implementation LSCircleListView`

`/*`

`// Only override drawRect: if you perform custom drawing.`

`// An empty implementation adversely affects performance during animation.`

`- (void)drawRect:(CGRect)rect {`

`// Drawing code`

`}`

`*/`

`#pragma mark - system`

`- (instancetype)initWithViewModel:(id)viewModel {`

`self.viewModel = (LSCircleListViewModel *)viewModel;`

`return` `[``super` `initWithViewModel:viewModel];`

`}`

`- (void)updateConstraints {`

`WS(weakSelf)`

`[self.mainTableView mas_makeConstraints:^(MASConstraintMaker *make) {`

`make.edges.equalTo(weakSelf);`

`}];`

`[``super` `updateConstraints];`

`}`

`#pragma mark - private`

`- (void)yc_setupViews {`

`[self addSubview:self.mainTableView];`

`[self setNeedsUpdateConstraints];`

`[self updateConstraintsIfNeeded];`

`}`

`- (void)yc_bindViewModel {`

`[self.viewModel.refreshDataCommand execute:nil];`

`@weakify(self);`

`[self.viewModel.refreshUI subscribeNext:^(id x) {`

`@strongify(self);`

`[self.mainTableView reloadData];`

`}];`

`[self.viewModel.refreshEndSubject subscribeNext:^(id x) {`

`@strongify(self);`

`[self.mainTableView reloadData];`

`switch` `([x integerValue]) {`

`case` `LSHeaderRefresh_HasMoreData: {`

`[self.mainTableView.mj_header endRefreshing];`

`if` `(self.mainTableView.mj_footer == nil) {`

`self.mainTableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{`

`@strongify(self);`

`[self.viewModel.nextPageCommand execute:nil];`

`}];`

`}`

`}`

`break``;`

`case` `LSHeaderRefresh_HasNoMoreData: {`

`[self.mainTableView.mj_header endRefreshing];`

`self.mainTableView.mj_footer = nil;`

`}`

`break``;`

`case` `LSFooterRefresh_HasMoreData: {`

`[self.mainTableView.mj_header endRefreshing];`

`[self.mainTableView.mj_footer resetNoMoreData];`

`[self.mainTableView.mj_footer endRefreshing];`

`}`

`break``;`

`case` `LSFooterRefresh_HasNoMoreData: {`

`[self.mainTableView.mj_header endRefreshing];`

`[self.mainTableView.mj_footer endRefreshingWithNoMoreData];`

`}`

`break``;`

`case` `LSRefreshError: {`

`[self.mainTableView.mj_footer endRefreshing];`

`[self.mainTableView.mj_header endRefreshing];`

`}`

`break``;`

`default``:`

`break``;`

`}`

`}];`

`}`

`#pragma mark - lazyLoad`

`- (LSCircleListViewModel *)viewModel {`

`if` `(!_viewModel) {`

`_viewModel = [[LSCircleListViewModel alloc] init];`

`}`

`return` `_viewModel;`

`}`

`- (UITableView *)mainTableView {`

`if` `(!_mainTableView) {`

`_mainTableView = [[UITableView alloc] init];`

`_mainTableView.delegate = self;`

`_mainTableView.dataSource = self;`

`_mainTableView.backgroundColor = GX_BGCOLOR;`

`_mainTableView.separatorStyle = UITableViewCellSeparatorStyleNone;`

`_mainTableView.tableHeaderView = self.listHeaderView;`

`[_mainTableView registerClass:[LSCircleListTableCell class] forCellReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([LSCircleListTableCell class])]];`

`WS(weakSelf)`

`_mainTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{`

`[weakSelf.viewModel.refreshDataCommand execute:nil];`

`}];`

`_mainTableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{`

`[weakSelf.viewModel.nextPageCommand execute:nil];`

`}];`

`}`

`return` `_mainTableView;`

`}`

`- (LSCircleListHeaderView *)listHeaderView {`

`if` `(!_listHeaderView) {`

`_listHeaderView = [[LSCircleListHeaderView alloc] initWithViewModel:self.viewModel.listHeaderViewModel];`

`_listHeaderView.frame = CGRectMake(0, 0, SCREEN_WIDTH, 160);`

`}`

`return` `_listHeaderView;`

`}`

`- (LSCircleListSectionHeaderView *)sectionHeaderView {`

`if` `(!_sectionHeaderView) {`

`_sectionHeaderView = [[LSCircleListSectionHeaderView alloc] initWithViewModel:self.viewModel.sectionHeaderViewModel];`

`}`

`return` `_sectionHeaderView;`

`}`

`#pragma mark - delegate`

`#pragma mark - UITableViewDataSource`

`- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {`

`return` `1;`

`}`

`- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {`

`return` `self.viewModel.dataArray.count;`

`}`

`- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {`

`LSCircleListTableCell *cell = [tableView dequeueReusableCellWithIdentifier:[NSString stringWithUTF8String:object_getClassName([LSCircleListTableCell class])] forIndexPath:indexPath];`

`if` `(self.viewModel.dataArray.count > indexPath.row) {`

`cell.viewModel = self.viewModel.dataArray[indexPath.row];`

`}`

`return` `cell;`

`}`

`#pragma mark - UITableViewDelegate`

`- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {`

`return` `100;`

`}`

`- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {`

`if` `(self.viewModel.dataArray.count > indexPath.row) {`

`[self.viewModel.cellClickSubject sendNext:self.viewModel.dataArray[indexPath.row]];`

`}`

`}`

`- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {`

`return` `self.sectionHeaderView;`

`}`

`- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {`

`return` `45;`

`}`

`@end`

 |

主View分为四个模块:

ⅰ 第一个模块 : 系统函数

17.png

每个View都会有对应的ViewModel,这样也更易复用,这里因为是主View,一般而言我都会使得VC和主V共用一个VM,这样对于跳转、数据共享等都有着极大的好处。

ⅱ 第二个模块 : 私有函数

18.png

具体作用途中已经标注,需要注意的是这些对于不同数据的处理,是我自己写的,逻辑上肯定没有那么缜密,仅供参考。

ⅲ 第三个模块 : 懒加载

19.png

这里没啥好说的,就是用的MJRefresh这个第三方库做的刷新。不过,假如你细心的话肯定会发现下面那两个View都是用VM来配置初始化的,这个和主View的配置初始化的意义是一样的。

ⅳ 第四个模块 : 代理及数据源

20.png

其中使用的是自定义Cell,用ViewModel来配置,点击事件也是和之前的VC的跳转联系起来了,并将VM传过去。

3、LSCircleListModel的处理

同样,先上代码

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

 | 

`//`

`//  LSCircleListModel.h

`//  ZhongShui`

`//`

`//  Created by 王隆帅 on 16/3/17.`

`//  Copyright ? 2016年 王隆帅. All rights reserved.`

`//`

`#import  `

`@interface LSCircleListModel : NSObject`

`@property (nonatomic, copy) NSString *idStr;`

`@property (nonatomic, copy) NSString *title;`

`@property (nonatomic, copy) NSString *intro;`

`@property (nonatomic, copy) NSString *img;`

`@property (nonatomic, copy) NSString *memberCount;`

`@property (nonatomic, copy) NSString *topicCount;`

`@end`

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

 | 

`//`

`//  LSCircleListModel.m`

`//  ZhongShui`

`//`

`//  Created by 王隆帅 on 16/3/17.`

`//  Copyright ? 2016年 王隆帅. All rights reserved.`

`//`

`#import "LSCircleListModel.h"`

`@implementation LSCircleListModel`

`+ (NSDictionary *)mj_replacedKeyFromPropertyName {`

`return`  `@{`

`@``"idStr"``:@``"id"``,`

`@``"title"``:@``"title"``,`

`@``"intro"``:@``"intro"``,`

`@``"img"``:@``"img"``,`

`@``"memberCount"``:@``"MemberCount"``,`

`@``"topicCount"``:@``"TopicCount"``,`

`};`

`}`

`@end`

这个就不贴图介绍了,就是单纯的数据模型,使用了MJExtention这个数据模型转换框架。没有做任何其他的逻辑处理。

4、ViewModel的处理

| 

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

 | 

`//`

`//  LSCircleListViewModel.h`

`//  ZhongShui`

`//`

`//  Created by 王隆帅 on 16/3/10.`

`//  Copyright ? 2016年 王隆帅. All rights reserved.`

`//`

`#import "YCViewModel.h"`

`#import "LSCircleListHeaderViewModel.h"`

`#import "LSCircleListSectionHeaderViewModel.h"`

`@interface LSCircleListViewModel : YCViewModel`

`@property (nonatomic, strong) RACSubject *refreshEndSubject;`

`@property (nonatomic, strong) RACSubject *refreshUI;`

`@property (nonatomic, strong) RACCommand *refreshDataCommand;`

`@property (nonatomic, strong) RACCommand *nextPageCommand;`

`@property (nonatomic, strong) LSCircleListHeaderViewModel *listHeaderViewModel;`

`@property (nonatomic, strong) LSCircleListSectionHeaderViewModel *sectionHeaderViewModel;`

`@property (nonatomic, strong) NSArray *dataArray;`

`@property (nonatomic, strong) RACSubject *cellClickSubject;`

`@end`
 |

| 

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

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

 

//  LSCircleListViewModel.m

//  ZhongShui

`//  Created by 王隆帅 on 16/3/10.

`//  Copyright ? 2016年 王隆帅. All rights reserved.

`//`

`#import "LSCircleListViewModel.h"

`#import "LSCircleListCollectionCellViewModel.h"`

`#import "LSCircleListModel.h"`

`@interface LSCircleListViewModel ()`

`@property (nonatomic, assign) NSInteger currentPage;`

`@end`

`@implementation LSCircleListViewModel`

`- (void)yc_initialize {`

`@weakify(self);`

`[self.refreshDataCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dict) {`

`@strongify(self);`

`if` `(dict == nil) {`

`[self.refreshEndSubject sendNext:@(LSRefreshError)];`

`ShowErrorStatus(@``"网络连接失败"``);`

`return``;`

`}`

`if` `([dict[@``"status"``] integerValue] == 0) {`

`self.listHeaderViewModel.dataArray = [[[([(NSDictionary *)dict[@``"res"``] arrayForKey:@``"JoinCircles"``]).rac_sequence map:^id(NSDictionary *dic) {`

`LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:dic];`

`LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];`

`viewModel.model = model;`

`return` `viewModel;`

`}] array] mutableCopy];`

`self.dataArray = [[[([(NSDictionary *)dict[@``"res"``] arrayForKey:@``"Circles"``]).rac_sequence map:^id(NSDictionary *dic) {`

`LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:dic];`

`LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];`

`viewModel.model = model;`

`return` `viewModel;`

`}] array] mutableCopy];`

`[self ls_setHeaderRefreshWithArray:dict[@``"Circles"``]];`

`[self ls_dismiss];`

`} ``else` `{`

`[self.refreshEndSubject sendNext:@(LSRefreshError)];`

`ShowMessage(dict[@``"mes"``]);`

`}        `

`}];`

`[[[self.refreshDataCommand.executing skip:1] take:1] subscribeNext:^(id x) {`

`@strongify(self);`

`if` `([x isEqualToNumber:@(YES)]) {`

`[self ls_showWithStatus:@``"正在加载"``];`

`}`

`}];`

`[self.nextPageCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dict) {`

`@strongify(self);`

`if` `(dict == nil) {`

`[self.refreshEndSubject sendNext:@(LSRefreshError)];`

`ShowErrorStatus(@``"网络连接失败"``);`

`return``;`

`}`

`if` `([dict[@``"status"``] integerValue] == 0) {`

`NSMutableArray *recommandArray = [[NSMutableArray alloc] initWithArray:self.dataArray];`

`for` `(NSDictionary *subDic ``in` `[(NSDictionary *)dict[@``"res"``] arrayForKey:@``"Circles"``]) {`

`LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:subDic];`

`LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];`

`viewModel.model = model;`

`[recommandArray addObject:viewModel];`

`}`

`self.dataArray = recommandArray;`

`[self ls_setFootRefreshWithArray:dict[@``"Circles"``]];`

`[self ls_dismiss];`

`} ``else` `{`

`[self.refreshEndSubject sendNext:@(LSRefreshError)];`

`ShowMessage(dict[@``"mes"``]);`

`}`

`}];`

`}`

`#pragma mark - private`

`- (NSMutableDictionary *)requestCircleListWithId:(NSString *)idStr currentPage:(NSString *)currentPage {`

`idStr = IF_NULL_TO_STRING(idStr);`

`currentPage = IF_NULL_TO_STRING(currentPage);`

`NSMutableDictionary * dict = [@{@``"MemberID"``: idStr, @``"pageSize"``: LS_REQUEST_LIST_COUNT, @``"pageIndex"``:currentPage} mutableCopy];`

`return` `dict;`

`}`

`- (void)ls_setFootRefreshWithArray:(NSArray *)array {`

`if` `(array.count < LS_REQUEST_LIST_NUM_COUNT) {`

`[self.refreshEndSubject sendNext:@(LSFooterRefresh_HasNoMoreData)];`

`} ``else` `{`

`[self.refreshEndSubject sendNext:@(LSFooterRefresh_HasMoreData)];`

`}`

`}`

`- (void)ls_setHeaderRefreshWithArray:(NSArray *)array {`

`if` `(array.count < LS_REQUEST_LIST_NUM_COUNT) {`

`[self.refreshEndSubject sendNext:@(LSHeaderRefresh_HasNoMoreData)];`

`} ``else` `{`

`[self.refreshEndSubject sendNext:@(LSHeaderRefresh_HasMoreData)];`

`}`

`}`

`#pragma mark - lazyLoad`

`- (RACSubject *)refreshUI {`

`if` `(!_refreshUI) {`

`_refreshUI = [RACSubject subject];`

`}`

`return` `_refreshUI;`

`}`

`- (RACSubject *)refreshEndSubject {`

`if` `(!_refreshEndSubject) {`

`_refreshEndSubject = [RACSubject subject];`

`}`

`return` `_refreshEndSubject;`

`}`

`- (RACCommand *)refreshDataCommand {`

`if` `(!_refreshDataCommand) {`

`@weakify(self);`

`_refreshDataCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {`

`@strongify(self);`

`return` `[RACSignal createSignal:^RACDisposable *(id subscriber) {`

`@strongify(self);`

`self.currentPage = 1;`

`[self.request POST:LS_URL_CIRCLE_MEMBER_LIST parameters:[self requestCircleListWithId:@``"1"` `currentPage:[NSString stringWithFormat:@``"%d"``,self.currentPage]] success:^(CMRequest *request, NSString *responseString) {`

`NSDictionary *dict = [responseString objectFromJSONString];`

`[subscriber sendNext:dict];`

`[subscriber sendCompleted];`

`} failure:^(CMRequest *request, NSError *error) {`

`ShowErrorStatus(@``"网络连接失败"``);`

`[subscriber sendCompleted];`

`}];`

`return` `nil;`

`}];`

`}];`

`}`

`return` `_refreshDataCommand;`

`}`

`- (RACCommand *)nextPageCommand {`

`if` `(!_nextPageCommand) {`

`@weakify(self);`

`_nextPageCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {`

`@strongify(self);`

`return` `[RACSignal createSignal:^RACDisposable *(id subscriber) {`

`@strongify(self);`

`self.currentPage ++;`

`[self.request POST:LS_URL_CIRCLE_TOPIC_LIST parameters:nil success:^(CMRequest *request, NSString *responseString) {`

`NSDictionary *dict = [responseString objectFromJSONString];`

`[subscriber sendNext:dict];`

`[subscriber sendCompleted];`

`} failure:^(CMRequest *request, NSError *error) {`

`@strongify(self);`

`self.currentPage --;`

`ShowErrorStatus(@``"网络连接失败"``);`

`[subscriber sendCompleted];`

`}];`

`return` `nil;`

`}];`

`}];`

`}`

`return` `_nextPageCommand;`

`}`

`- (LSCircleListHeaderViewModel *)listHeaderViewModel {`

`if` `(!_listHeaderViewModel) {`

`_listHeaderViewModel = [[LSCircleListHeaderViewModel alloc] init];`

`_listHeaderViewModel.title = @``"已加入的圈子"``;`

`_listHeaderViewModel.cellClickSubject = self.cellClickSubject;`

`}`

`return` `_listHeaderViewModel;`

`}`

`- (LSCircleListSectionHeaderViewModel *)sectionHeaderViewModel {`

`if` `(!_sectionHeaderViewModel) {`

`_sectionHeaderViewModel = [[LSCircleListSectionHeaderViewModel alloc] init];`

`_sectionHeaderViewModel.title = @``"推荐圈子"``;`

`}`

`return` `_sectionHeaderViewModel;`

`}`

`- (NSArray *)dataArray {`

`if` `(!_dataArray) {`

`_dataArray = [[NSArray alloc] init];`

`}`

`return` `_dataArray;`

`}`

`- (RACSubject *)cellClickSubject {`

`if` `(!_cellClickSubject) {`

`_cellClickSubject = [RACSubject subject];`

`}`

`return` `_cellClickSubject;`

`}`

`@end`

ViewModel也是分为三个模块,由于代码太多摘重要的讲

ⅰ 第一个模块 : 处理数据、逻辑模块

01.png

处理数据这块,先用字典转为Model,在用Model配置ViewModel,ViewModel再去与UI及其逻辑对应。

ⅱ 第二个模块 : 私有函数

02.png

对于请求参数字典,可以放在VM中,便于模块化移植,也可以放在公共API中便于管理,看个人选择了,没有绝对的好位置,只有更适合个人的位置。

另外两个函数就是处理下拉及上拉时有没有更多数据的私有函数。

ⅲ 第三个模块 : 懒加载

03.png

此数据请求用的是AFNetworking。

5、APPDelegate的代码简化

一般而言,我们正式项目中会遇到很多需要启动项目时就加载的,所以很快APPDelegate就会越来越庞大,既然其他的代码都简化解耦了,这里也可以做一下处理。

目录如下:

1686195-2abc3f69ae0aa8fd.jpg

简化后的AppDelegate如下:

04.png

其他代码存放的位置如下:

05.png

当类对象被引入项目时, runtime 会向每一个类对象发送 load 消息. load 方法还是非常的神奇的, 因为它会在每一个类甚至分类被引入时仅调用一次, 调用的顺序是父类优先于子类, 子类优先于分类. 而且 load 方法不会被类自动继承, 每一个类中的 load 方法都不需要像 viewDidLoad 方法一样调用父类的方法。

这是利用了这个算是黑魔法的玩意,哈哈,就简化了APPDelegate!

四、后记

当初本来想干掉基类来着,想利用Category + Protocol并利用Runtime的Methode Swizzle 来给系统函数添加自己的私有函数,当初VC已经搞定了,然而发现这样牵涉面太广,你对VC做了Category,UINavigationController 也会受到影响,假如你对View做了Category,其他继承View的也会有影响,而且当时交换方法都是在一个Category里管事,到第二个就覆盖了。。。不造为啥,因为知道这条路走不通就没继续搞下去了。。。

写到这里,大家应该都对我笔下的架构模式有了一些了解,因为里面涉及的东西确实太多,主要是这些玩意需要站在巨人的肩膀,遇到文中没有提到而且不懂得可以:

06.png

哈哈哈!别怪我...不是我不负责,因为你可以看看写到这里篇幅已经超出常人所能接受的了,而且我感觉我把各个细节已经都照顾到了吧(?乛?乛? 磨人的小妖精)!大家有什么疑惑我们可以在评论区交流!

最后,真的很希望各位大神指出不足的地方,能让大家共同进步!

五、Demo已出(动动你的手指,赞一下撒!)

代码地址:https://github.com/wanglongshuai/MVVM-RAC-Demo

代码地址:https://github.com/wanglongshuai/MVVM-RAC-Demo

原文章地址 因为在别的平台怕没就存下来了

http://www.cocoachina.com/ios/20170213/18659.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容