电商项目(MVVM+ReactiveCocoa)

电商项目(MVVM+ReactiveCocoa)_第1张图片

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处理网络请求,网络请求的最终结果通过ViewModelRACComand信号传递到控制器。

  • 控制器持有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的普通控制器作为根控制器,在BaseViewControllerviewDidLoad方法里将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属性是子类父类的关系,意味着子类控制器去调用BaseViewControllerViewModle属性的方法,会造成水土不服。

  • 解决方法就是在继承于BaseViewController的子类控制器里面声明@dynamic viewModel,如此一来,ViewModel所属Class不再是BaseViewModel,而是属于子类Class。这样来看,并没有起到父类帮子类声明统一属性的方法,如果子类用的属性跟父类用的属性声明的方法相同,自然没问题,可是子类的属性与父类的属性是子类父类的关系,这就需要子类改写父类的成员变量的类型,变量名称不变,Xcode提示你父类和子类声明了重复的成员变量,使用@dynamic告诉Xcode成员变量所属Class的关系就可以了。

  • BaseViewController本来不该写这个属性的,但是不得已而为之,十分需要BaseViewModel属性来保存实例控制器时传进来的ViewModel,必须依赖baseViewController.baseViewModel.jumpTool保存navigationController才能实现控制器的跳转。否则,每一个继承于BaseViewController基类控制器的子类控制器都需要重写一些基类的init方法,不然根本就没法保存初始化控制器时传进来的viewModel。其实后来的通过子类Class再次声明viewModel和@dynamic本质就是为了扩展基类BaseViewControllerviewModel属性的方法和属性。

  • 由类方法初始化控制器变成了由类名字符串[[NSClassFromString(ClassName) alloc]来初始化控制器。封装控制器跳转逻辑的唯一原因就是统一规避所有可能会遇到的控制器跳转异常,异常主要有两点:一是导航控制器不存在,二是待跳转的控制器构建不出来。
商品Cell
  • Cell下方存在加入购物车的逻辑,扯出了购物车管理器、购物车管理器又扯出了用户管理器、用户管理器有扯出了地址管理器、现在迷失在地址管理器,难以自拔!
RACComand
  • UIButton扩展了rac_command的属性,将buttonenable状态与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是需要以RACCommandBlock形参返回值返回出去的。冷信号RACsignalBlock形参的返回值则是一个RACDisposable对象,唯一的意义就是在订阅者subscriber发布错误信号error或是结束信号complete之后会销毁我们创建的RACsignal冷信号,销毁这个信号的同时会进入RACDisposableBlock参数代码块里面。如果需要在冷信号被销毁之后执行某些代码,那么RACDisposable显得特别暖心,平时直接return一下RACDisposable的实例就可以了。

  • 接下来就是逻辑处理异步请求的结果,方式一直接在异步耗时操作的Block回调进行处理;方式二是订阅这个RACSignal冷信号,然后根据不同的信号值,做出处理。

  • 登录注册首先实例RACCommand,后面的Block代码块里面直接就return创建RACSignal冷信号。RACSignalBlock代码块里,就是异步网络请求,同时类方法Block回调网络请求结果,请求成功则subscriber发送YES,反之失败则subscriber发送NO

  • 创建RACsignal冷信号带有一个Block代码块。RACDisposable作为与异步网络请求并行的关系必须作为Block代码块的返回值进行return

  • 订阅RACComandBlock代码块里面创建的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]这两个单例类模型对象重新保存到本地沙盒,保证退出应用,购物车数据依然存在。

你可能感兴趣的:(电商项目(MVVM+ReactiveCocoa))