MVVM架构
自然是按照ViewModel、ViewController、View、Model
的顺序依次构建。
RACObserve循环引用
self.rac_willDeallocSignal信号—>block—->self—->rac_willDeallocSignal
信号
MVVM构造更加轻量化ViewController
控制器里面所有属于事件处理的逻辑和计算通通搬到
ViewModel
里面去,换句话说,控制器就干两件事情,第一、响应用户的交互;第二,呈现给用户交互结果。问题的复杂性就在于此,典型的一个购物车,你选中一个商品,这是一个交互,而交互的结果却是多个的,包括总价格要变化,按钮的使能要变化,商品的总数要发生变化,交给
ViewModel
来处理,好了你至少要计算出三个交互结果,然后三次使用Block
或是委托来实现逆向传值,才能把结果ViewModel
的处理结果传递到控制器去,如果不是三个而是5个6个,那很有可能带来的结果是,本来是想精简控制器的代码所以设置ViewModel
,最后因为事件处理的结果和多,使用了一大堆的Block
和委托来传值,反而把控制器给弄乱了,因此很多开发者如果不会使用RAC却使用MVVM
的框架是很考验耐心的,还不如直接把事件处理写在控制器来的直接呢,虽然控制器冗余了点,但是还谈不上乱呀。如果使用
RAC
来处理控制器和ViewModel
之间的协作,所有的问题迎刃而解。原因就是用户交互终究只会触发一个信号,无论最后这个用户交互事件需要呈现多少个连锁反应,所有与这个操作有关的UI
元素订阅这个信号不就行了么,一个信号对应多个连锁反应,就是这么神奇。那么问题来了,都有哪些方法把控制器的事件传递到
ViewModel
里面去呢 ?现在最常用到的就是第一:RACObserve
观察者主要传递控制器的输入内容变化;第二:RACCommand
主要用于耗时事件;第三:RACSubject
用于立即响应按钮事件,例如简单地跳转个控制器啥的。
ViewModel和ViewController的交互
ViewModel
处理网络请求,网络请求的最终结果通过ViewModel
的RACComand
信号传递到控制器。控制器持有
ViewModel
对象,意味着控制器持有ViewModel
对象的所有属性,ViewModel
对象的属性可是实时通过RACObserver
记录控制器的值,对ViewModel
的属性做更新监听,一旦属性变化一次,就释放一次信号,进行判断后给控制器一个反馈信号。完整的逻辑链条就是输入框输入内容,释放以内容为值的信号,
ViewModel
的属性绑定了这个信号,将输入框值自动绑定了自己的属性上、ViewModel
的另一个信号属性又订阅了这个属性的值的变化,一旦变化,则发送一个信号出去,然后控制器的输入框和登录按钮的使能状态又订阅了这个信号、再然后就是一个完整的响应链条,实现控制器最初发送信号,ViewModel
处理信号,然后再发出信号,控制器再响应信号的闭环生态。简单地说,控制器发A信号同时订阅
ViewModel
处理完A信号发送的B信号。如果需要及时反馈,那么直接发送
RACSingle
信号,如果对于A信号的处理是一个异步耗时操作,那么RACCommand
命令内嵌RACSingle
的形式返回B信号。RACComand
里面的信号执行异步耗时操作,返回成功或失败,成功就发送值为字典的信号,失败就发送错误信号。
BaseViewModel
BaseViewModel
的属性包括title、jumpTool、paramsDict
。初始化BaseViewModel
时就需要传入paramsDict
,意义在于self.title = paramsDict[@"title"]
给控制器的导航栏标题赋值,除此之外,paramsDict
还可以传递其它的参数,当然是以字典键值对的形式进行传递。jumpTool
作为一个继承于NSObject
的对象,持有navigationController
属性值,封装一系列控制器跳转的方法,意义在于处理navigationController
或是待跳转viewController
不存在时的异常。baseViewModel
作为初始化baseViewController
的形参,意味着初始化baseViewcontroller
之前必须先实例化一个baseViewModel
,因为baseViewModel
里面就持有baseViewController
所需要的所有数据,包括最重要的导航栏标题,这就是相对于属性正向传值的优点,可以在初始化viewController
的时候就把数据赋值给控制器。当然为了全局的使用和对传进来的viewModel
进一步操作,就需要将初始化控制器传进来的viewModel
赋值给self.baseViewModel
。MVC
都是在viewController
创建控制器,然后self.navigationController
跳转控制器。为了在viewModel
里面跳转控制器,这里则是通过ViewModel.jumpTool
来调用方法跳转控制器。 一个tabarItem对应着一个navigationController
,创建navigationController
导航控制器的时候,需要初始化一个继承于BaseViewController
的普通控制器作为根控制器,在BaseViewController
的viewDidLoad
方法里将navigationController
存在baseViewModel.jumpTool.navigation
属性里。如此一来,JumpTool
控制器跳转帮助类就可以在持有navigationController
的基础上随意封装任何形参的控制器跳转方法。只要谁持有了控制器跳转帮助类对象,谁就可以在任意地方执行控制器跳转。
继承于
BaseViewModel
基类的viewmodel都会重写基类的init方法,意义是子类ViewModel
相比父类BaseViewModel
拥有更多的属性需要初始化,而且必须是在初始化ViewModel
对象的同时就来实例子类里面的这些新增属性。扩展这些属性都挺简单的的,能想到的唯一稍微麻烦的地方就是初始化VeiwModel
的新增RACComand
属性了。UI控件持有
ViewModel
目的是利用viewModel
响应控制器的用户交互事件或者说是为了将触发事件传递到viewModel里,比如按钮点击命令啥的。
BaseViewcontroller
首页控制器是通过
StoryBoard
初始化的,意味着没有按照BaseViewController
基类里面的初始化方法进行初始化。添加了右滑退出控制器的手势,这对于控制器的退出是十分重要且必要的。BaseViewController
父类里的初始化方法写了重要的一步,传入navigationController
参数初始化BaseViewModel
属性。自然ViewMode
属性值为空,自然在调用父类viewDidLoad
方法给BaseViewModel.jumpTool
属性赋值无法实现,因此难以跳转。BaseViewController
设置ViewModel
属性,因为每个子类控制器都会设置这个ViewModel
属性,干脆在基类里面设置ViewModel
属性,带来的问题就是子类的ViewModel
属性本质上与BaseViewController
里面的viewModel
属性是子类父类的关系,意味着子类控制器去调用BaseViewController
的ViewModle
属性的方法,会造成水土不服。解决方法就是在继承于
BaseViewController
的子类控制器里面声明@dynamic viewModel
,如此一来,ViewModel
所属Class
不再是BaseViewModel
,而是属于子类Class
。这样来看,并没有起到父类帮子类声明统一属性的方法,如果子类用的属性跟父类用的属性声明的方法相同,自然没问题,可是子类的属性与父类的属性是子类父类的关系,这就需要子类改写父类的成员变量的类型,变量名称不变,Xcode
提示你父类和子类声明了重复的成员变量,使用@dynamic
告诉Xcode
成员变量所属Class
的关系就可以了。BaseViewController
本来不该写这个属性的,但是不得已而为之,十分需要BaseViewModel
属性来保存实例控制器时传进来的ViewModel
,必须依赖baseViewController.baseViewModel.jumpTool
保存navigationController
才能实现控制器的跳转。否则,每一个继承于BaseViewController
基类控制器的子类控制器都需要重写一些基类的init方法,不然根本就没法保存初始化控制器时传进来的viewModel
。其实后来的通过子类Class
再次声明viewModel和@dynamic
本质就是为了扩展基类BaseViewController
的viewModel
属性的方法和属性。
- 由类方法初始化控制器变成了由类名字符串
[[NSClassFromString(ClassName) alloc]
来初始化控制器。封装控制器跳转逻辑的唯一原因就是统一规避所有可能会遇到的控制器跳转异常,异常主要有两点:一是导航控制器不存在,二是待跳转的控制器构建不出来。
商品Cell
- Cell下方存在加入购物车的逻辑,扯出了购物车管理器、购物车管理器又扯出了用户管理器、用户管理器有扯出了地址管理器、现在迷失在地址管理器,难以自拔!
RACComand
UIButton
扩展了rac_command
的属性,将button
的enable
状态与command
命令的执行来状态绑定,用户点击按钮时,command
命令会自动执行,同时按钮enable
置为NO
。如果手动执行command
命令,则可以发送参数。使用
RACComand
命令的时候的传入的参数,这个参数到底是什么地方传进来的,有什么用,就像属性传值是的,只要有RACComand
这个对象,就意味着可以把对象传递到RACComand
里面去。谁调用excue
这个消息,谁就有资格传递信息进行正向传值。使用
RACComand
的过程中,如果只是创建了一个信号,那么直接返回这个信号就可以了,同时这个信号在异步操作执行完成之后发送执行结果给那些订阅此信号的人。信号在把消息发送出去的同时,也销毁了信号本身,一旦RACComand
检测到内部持有的信号已经销毁,必然改变自己的执行状态,表示信号里面的耗时任务执行完毕。于是RACComand
的状态改成了执行完毕。那么现在是一个RACConmand
里面三个信号,信号3是一个刷新UI的信号,依赖信号1和信号2去请求数据,信号1和信号2同时成功后再去触发信号3。
RACCommand
的初始化必须实现Block
参数代码块,而且这个Block
参数代码块很特别,既有输入值id
类型数据,又有输出值RACSignal
,输入值来自于ViewModel
操作RACCommand
属性执行Excute
命令时,可以带一个参数过来,这个参数可以是任何形式,用来区分这个命令操作到底是属于谁。返回值必须是一个信号,就算你什么都不做,也必须在Block
参数代码块里面返回一个[RACSignal empty]
空信号,这就是一个典型的有输入值有返回值的Block
函数形参了。如果是要在
RACCommand
事件里面进行一个异步操作,就不能返回空信号了,不能返回空信号,必须返回一个冷信号,冷信号作用就是先把信号创建出来,暂且不发送任何的内容,直接在创建信号后的Block
形参代码块里面写入将要执行的异步操作,然后根据异步操作的执行结果通过订阅者subscriber
发送不同的信号值,当然我们创建的冷信号RACsignal
是需要以RACCommand
的Block
形参返回值返回出去的。冷信号RACsignal
的Block
形参的返回值则是一个RACDisposable
对象,唯一的意义就是在订阅者subscriber
发布错误信号error
或是结束信号complete
之后会销毁我们创建的RACsignal
冷信号,销毁这个信号的同时会进入RACDisposable
的Block
参数代码块里面。如果需要在冷信号被销毁之后执行某些代码,那么RACDisposable
显得特别暖心,平时直接return
一下RACDisposable
的实例就可以了。接下来就是逻辑处理异步请求的结果,方式一直接在异步耗时操作的
Block
回调进行处理;方式二是订阅这个RACSignal
冷信号,然后根据不同的信号值,做出处理。
登录注册首先实例
RACCommand
,后面的Block
代码块里面直接就return
创建RACSignal
冷信号。RACSignal
的Block
代码块里,就是异步网络请求,同时类方法Block
回调网络请求结果,请求成功则subscriber
发送YES
,反之失败则subscriber
发送NO
。创建
RACsignal
冷信号带有一个Block
代码块。RACDisposable
作为与异步网络请求并行的关系必须作为Block
代码块的返回值进行return
。订阅
RACComand
的Block
代码块里面创建的RACSignal
。方式一subscriber
订阅RACCommand.executionSignals.switchToLatest
信号;方式二直接在Blcok
代码块里subscriber
订阅前面创建的RACSignal
信号。获取
RACCommand
执行状态。方式一subscriber
订阅[RACCommand.executing skip:1]信号来判断RACCommand
执行状态。方式二直接在创建RACCommand
时默认RACCommand
执行状态开始,在订阅到RACSignal
发出值信号或RACSignal
被销毁时默认执行状态结束。subscriber
发送的值信号格式。如果网络请求数据成功,就发送@{@"code":@100,@"data":responseObject}
这个字典,如果请求数据失败,就发送@{@"code":@400,@"data":@"请求失败"}
RACSubject
通过
RACSubject
信号把View
子视图上的按钮事件传递到ViewModel
中区,同时传递的参数主要按钮的标识符Tag
号。类似于给View
子视图添加按钮事件Block
回调。TableView
子视图持有ViewModel
,在Cell
点击回调方法里将Cell
点击事件转变成RACSubject
信号发送到ViewModel
。
购物车数据本地化
读取。初始化购物车的
ViewModel
时订阅全局ShoppingManager
单例类change
属性值RACObserve([ShoppingManager manager],change)
信号,获取[ShoppingManager manager]
的goodsDic
数据更新UI
。保存。单例类的意义在于实时读写数据。以商品模型
id
为键、商品模型model
为值存入单例的[ShoppingManager manager].goodsDic
属性中。添加。当用户在
GoodManagerView
视图上点击增加或减少商品数量的时候,重写[ShoppingManager manager].goodsDic
。更新。首先
array
数组保存[ShoppingManager manager].goodsDic
字典的所有值allValue
,一个值代表一个商品模型model
,字典所有模型转移到数组之后清空goodsDic
;遍历商品模型数组array
的每一个商品模型model
,通过model.isSelected
判断商品是否被选中;未选中状态则继续以商品模型id
为键、模型model
为值存入[ShoppingManager currentUser].goodsDic
字典中;处于选中状态则首先获取被选中商品模型model
的单个商品数量,更新用户管理单例类的[UserManager currentUser].bageValue
商品总数属性;最后,在商品模型数组array的每一个商品模型model都遍历一遍之后,需要将[ShoppingManager manger]
和[UserManager currentUser]
这两个单例类模型对象重新保存到本地沙盒,保证退出应用,购物车数据依然存在。