导语
使用RAC实践购物车逻辑的梳理!首先分析ViewModel
里面的每一个属性,然后分析ViewModel
里面的每一个方法,再然后就是从ViewController
的头文件引用中去发现View和Model
的联系。
心中就有一个疑问,其实总体上来说,ViewModel
终究只是ViewController
和View
和Model
之间的粘连剂量,也就是说,必须先整理出一系列的需求才是通常情况下设置ViewModel
的依据呀,如果没有需求根本没法确定ViewModel
,所以首先是发现所有的需求。
整理购物车的思路就是根据MVVM的结构来推导,首先就是推导ViewModel
,这才是一个控制器的核心,因为他直接反应了这个控制器最核心的诉求或者说是需求,只要弄懂ViewModel
,就能抓住控制器的逻辑思路,就能够分析出完整的逻辑链条,问题是,自己的逻辑链条是通常都是涉及了多个模块,需要多个模块的协作共同完成,因此我要做的就是根据逻辑来梳理MVVM之间的协作关系,而不是将一个完整的逻辑链条强制拆成MVVM的四个模块,这严重不能展示出ViewModel的粘连剂的作用。
所以更具需求梳理出逻辑线来,才是最佳的方案。而逻辑链条,最好的查看逻辑就是ViewController
的绑定ViewModel
方法了,这里面绑定了什么,就能知道有什么逻辑了。
AddSubView
-
registerNib:[UINib nibWithNibName:]
注册Xib
类型的Cell
复用
-registerClass:NSClassFromString(@"") forHeaderFooter
注册sectionHeader
和sectionFooter
设置
dataSource
和delegate
为一个UIService
类对象,这就意味着tableView
的所有回调事件通通都写到了UIService
类里。唯一需要考虑的问题,就是如何把reloadData
需要的dataArray
数据传递到UIService
类,dataArray
作为viewModel
的属性,如果直接在初始化的时候,令service.dataArray = viewModel.dataArray
,那么假如viewModel
请求数据刷新了viewModel.dataArray
里面的值,然后执行reloadData
方法就会发现service.dataArray
数据源并没有更新,根本的原因就是service.dataArray
只是记录了初始化service
时的viewModel.dataArray
值。所以直接在初始化service
时,令service.viewModel = viewModel,
如此一来,无论viewModel.dataArray
变成什么样,service
总能获取到最新的dataArray
数据。除此之外,service
里的用户交互事件也需要viewModel
传递出去,典型的cell选中或cell左滑删除。自定义
ShopCartBarView
,常规的添加结算Button
、全选Button
、价格Lable
抛开不谈,重点是ShopCartBarView
如何实时刷新价格Lable
的text
属性。常规的方法是在将价格Lable
属性暴露在.h头文件里或者把想要展示的字符串想方设法传递到.m文件里。现在更新价格Lable直接是KVO监听ShopCartBarView
的money
属性值的变化,一旦发现self.money
属性值发生变化,立马在RACObserve
的subscribeNext
方法里更新价格Lable
。那么,只要ShopCartBarView
的money
属性值发生改变,立马就能呈现在价格Lable
上,这相当于添加了一个步骤,原本赋值时通过view.Lable.text
,现在则是通过改变view.money
,后面的self.lable.text = self.money
通过subscribeNext
订阅RACObserve(self, money)
来实现。view.Lable.text
必须明明白白告诉Lable
需要展示的内容,但是使用view.money
只需要把展示的内容告诉money
属性存起来,后来的展示具体过程可以通过内部实现,前者是亲力亲为,后者是下达命令。这两者对比起来差别还是蛮大的。这样的好处在于如果仅仅是更新一个
Lable
数据来说,那么确实的,view.Lable.text
还是view.money
这两个方法差别并不大,真正的区别在于前者有局限性,后者有着更加灵活的处理方式。典型的,如果view.button.enable
也严格与money
属性的值有关,自然第二种方法效果更好,可能单一个逻辑链条提现不出来RAC的优势,但是一旦变成多重连锁反应,那么毫无疑问,RAC
最有优势,RAC
也就是为处理多重影响而生。如果不用RAC
,一个money
值变化需要处理后持续两个逻辑,第一刷新Lable
内容,第二修改Button
状态。这必然就会想到通过重写set方法来实现,也能达成目的,但是如果刷新Lable
和修改Button
这两重逻辑还有先后顺序和逻辑依赖呢,这就麻烦了呀!如果一个BOOL状态的变化承载了一系列连锁的反应或多重的影响,毫无疑问,
subscribeNext
订阅RACObserve(self, BOOL)
的信号,然后执行所有的影响和逻辑操作。
数据刷新
直接
self.viewModel
调用方法获取初始数据并存入viewModel.dataArray
。获取数据两种方式,方式一调用getData普通方法,方式二执行
refreshCommand
命令。viewModel.view
刷新UI
业务逻辑
数组遍历。
rac_sequence
将可变数组变成序列、map
遍历序列里的每一个元素生成新的序列、array
将序列变成数组、mutableCopy
将普通数组变成可变数组。-
商品全选
第一次初始加载数据的时候,每一个组
section
的选中状态都是存储在一个数组之中的。但是这也只是对第一次初始加载时有用,以后如果section
选中状态的数组被更新,也必须想要把数组里面的section
选中状态修改后再执行raloadDataSection
方法才有效。subscribeNext
订阅self.view.button
的点击事件,self.viewModel
计算商品全选逻辑。商品全选时执行的逻辑包括:重置商品状态数组、重置大数组的小数组的每一个Model
、计算所有商品的总金额存到self.allPrices
属性、viewModel.view
刷新UI。需要考虑一种特殊情况,用户根本不点击全选按钮,只是简单地把所有的商品都勾选一遍,这个时候置全选按钮于选中状态?很简单,
RAC(self.view.button, selected) = RACObserve(self.viewModel,isSelectAll)
相当于button.selected = self.viewModel.isSelectAll,
只要isSelectAll
属性值发生改变,就发送一个信号,这个赋值的方法就会被调用一次,这其实就间接解决了刚才viewModel.dataArray
数据更新但是UIService.dataArray
依然是初始数据的问题了。这就相当于使用间接的方法来先改变viewModel.isSelectAll
继而改变viewController.view.button.selected
属性值。
-
商品删除
方法一先勾选即将删除的商品,然后
subscribeNext
订阅self.view.deleteButton
的点击事件,self.viewModel
统一计算商品删除逻辑:1、遍历dataArray
中的每一个sectionArray
,接着遍历sectionArray
的每一个商品模型model
,判断model
选中状态,如果model
处于选中状态使用NSMutableIndexSet
集合对象存储当前model
的index
序号,在sectionArray
遍历完所有的·model·根据序号集合removeObjects
。这里面考虑一个特殊情况,就是如果删除一个sectionArray
里面的所有model
,必须从dataArray
中把这个sectionArray
也删掉。判断的一句依据就是选中的模型的序号集合的个数等于sectionArray
的元素个数。方法二直接左滑删除购物车商品,然后在cell的左滑事件里通过self.viewModel计算单个商品的删除逻辑:获取indexPath用来定位dataArray中的Model并从dataArray中remove掉、判断当前section的cell=0决定是reloadData还是reloadSections(
如果reloadData意味着删除cell组头选中状态数组和删除dataArray中的小sectionArray
)、更新购物车商品数量存到self.count
属性、调用getAllPrices
方法重新计算购物车总金额并存到self.allPrices
属性。
-
总共金额
计算总金额,直接相当于把
dataArray
中的没有商品Model
遍历一遍。判断Section
勾选状态数组元素个数与dataArray
中sectionArray
元素个数的关系,判断当前是否是全选状态并存入self.isSelectAll
属性。遍历dataArray
中的每一个sectionArray
,接着遍历sectionArray
中的每一个Model,filter
过滤掉未选中的Model,map
操作每一个处于选中状态的Model
,计算该商品模型model
的总金额并以@()数组元素的形式返回到数组之中,通过RAC
的array
方法将序列转变成存满金额的数组,最后遍历这个数组对所有元素进行相加,如此得到的购物车被选中所有商品的总共金额并存入self.allPrices
属性中。总金额实时更新到viewController.view.lable。text属性上。还是老套路,直接
RAC(vc.view.label, text) = RACObserve(vc.viewModel,allPrices)
。哈哈,错了,错的一塌糊涂,这样根本不行,直接就会导致程序的崩溃,问题的关键就是上面的RAC简写订阅方法只适用于绑定BOOL属性。其它属性还得是正常的写法subscribeNext订阅RACObserve(vc.viewModel,allPrices)属性值改变的信号。
商品数量。老套路,subscribeNext订阅
RACObserve(vc.viewModel,counts)
属性值改变的信号。-
Cell的HeaderView
service作为View的委托,意味着View上所有的参数回调设置和用户触发事件都在service对象里,
service
本质上只是帮助viewController
分担委托代理对调等方法,并不处理用户触发事件的逻辑,因此必须通过service.viewModel
将用户交互事件传递到viewModel
中去。headerview.button点击。1、订阅headerview.button的点击事件时,takeUntil跳过
headerView.rac_prepareForReuseSignal
准备复用前的信号,难道是说headerView在rac_prepareForReuseSignal
也要发送信号,headerview.button
的按钮点击信号和headerView.rac_prepareForReuseSignal
信号相互干扰,所以必须跳过。2、viewModel
执行headerView.button
点击事件,传递headerView的isSelected
选中状态和section
序号到viewModel
的方法里,
- Cell的SubView
cell.selectedButton被点击:传递
cell
的indexPath
和buttotn.selected
状态到viewModel
的方法里,然后找出indePath
对应的Model
并重置该model的isSelected
属性,同时判断的当前sectionArray
中的model
总数和已经被选中的model
进行比较,如果相等,记得修改记录section是否选中状态的数组为YES
,表示这一个组的Cell
都是被选中了。然后接下来就是reloadSections
只刷新这一section的数据。同时调用方法重新计算该购物车所有的商品金额保存到viewModel.allPrice
属性,因为viewController.Lable.text
订阅了viewModel的allPrice
属性,因此,只要allPrice被重新赋值,viewController.lable.text
立马被更新。cell.view.button被点击,这个时候是在
cell
上添加了一个View
,这个view
上面是两个按钮,一个➕,一个➖,那么自然就需要将这个两个按钮事件传递到cell
中去,cell拿着这两个按钮事件又来对Model
的单个商品数量进行加减,其实没有这个必要,直接在view
里面对单个商品的已有数量进行加减,直接将加减后的单个商品数量传递到Cell里面就可以了嘛!将单个商品的数量传递到cell的方式包括:第一通过Block
传递,每当订阅到按钮事件的信号后,就将保存的商品数量进行加减,加减完成后,通过Block
将加减后的单个商品数量传递到cell里面。第二就是在cell里面通过RACObserve(view,number)
订阅view.number
属性值的变化,一旦发生变化,就进入通过viewModel
调用方法处理单个商品数量变化的逻辑。cell.view.textfield被编辑。这就是一个特别好的示例了,
cell
上面添加了View,View
上面不仅添加了Button
,更添加了TextField
,无论是Button
还是TextField
都需要进行交互,然后通过viewModel
处理被点击还是被编辑所带来的深层次影响。文本框的RAC
处理。两种方式,方式一通过RAC(viewController.viewModel, password) = TextField.rac_textSignal
从TextField
开始编辑开始,只要字符变化一下,viewModel
就会立即subscribeNext
订阅到RACObserve(self,password)
的值信号,这个是值就是当前TextField
的最新字符串。方式二是subscribeNext
订阅[NSNotificationCenter defaultCenter] rac_addObserverForName:@"UITextFieldTextDidEndEditingNotification" object:TextField]
的信号,只有当TextField结束编辑之后才会发送TextField
这个UI
控件为值的信号,在cell.view
里订阅到这个信号之后,判断TextField.text
与总库存的关系,如果大于总共的库存,直接Block
返回单个商品的库存数量,其它数量,直接Block返回就行。一谈到TextField
必然会涉及到text属性值与Color
和button.selected
的绑定,这都是套路,订阅RACObserve(self, number)中number
属性值的变化,减号button.selected = @(number>1)
,加号button.selected = @(number<库存)
。RAC(TextField,textColor) = 库存?[UIColor blackColor]:[UIColor redColor];
最后,有很重要的一点就是需要将Block
回掉到cell的单个商品数量同时保存在cell.view.number
属性里,因为数据的刷新还得是依靠Cell
的reloadData
。单个商品数量变化的深层次逻辑。当单个商品数量通过
Block
回调到Cell
之后,cell是不能负责因为当个商品数量变化引起的控制器逻辑变化,必须继续把当个商品数量和Cell
的IndexPath
以参数的形式传递到ViewModel
中进行处理。逻辑包括:根据IndexPath
定位Cell
的Model
,修改重置Model的单个商品数量属性,修改模型后就,可以reloadSection
改组的所有Cell
数据,最后重新计算当前所有被选中商品的总金额。