电商购物(MVVM+ReactiveCocoa)

导语

使用RAC实践购物车逻辑的梳理!首先分析ViewModel里面的每一个属性,然后分析ViewModel里面的每一个方法,再然后就是从ViewController的头文件引用中去发现View和Model的联系。

心中就有一个疑问,其实总体上来说,ViewModel终究只是ViewControllerViewModel之间的粘连剂量,也就是说,必须先整理出一系列的需求才是通常情况下设置ViewModel的依据呀,如果没有需求根本没法确定ViewModel,所以首先是发现所有的需求。

整理购物车的思路就是根据MVVM的结构来推导,首先就是推导ViewModel,这才是一个控制器的核心,因为他直接反应了这个控制器最核心的诉求或者说是需求,只要弄懂ViewModel,就能抓住控制器的逻辑思路,就能够分析出完整的逻辑链条,问题是,自己的逻辑链条是通常都是涉及了多个模块,需要多个模块的协作共同完成,因此我要做的就是根据逻辑来梳理MVVM之间的协作关系,而不是将一个完整的逻辑链条强制拆成MVVM的四个模块,这严重不能展示出ViewModel的粘连剂的作用。

所以更具需求梳理出逻辑线来,才是最佳的方案。而逻辑链条,最好的查看逻辑就是ViewController的绑定ViewModel方法了,这里面绑定了什么,就能知道有什么逻辑了。

AddSubView

  • registerNib:[UINib nibWithNibName:]注册Xib类型的Cell复用

-registerClass:NSClassFromString(@"") forHeaderFooter注册sectionHeadersectionFooter

  • 设置dataSourcedelegate为一个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如何实时刷新价格Labletext属性。常规的方法是在将价格Lable属性暴露在.h头文件里或者把想要展示的字符串想方设法传递到.m文件里。现在更新价格Lable直接是KVO监听ShopCartBarViewmoney属性值的变化,一旦发现self.money属性值发生变化,立马在RACObservesubscribeNext方法里更新价格Lable。那么,只要ShopCartBarViewmoney属性值发生改变,立马就能呈现在价格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集合对象存储当前modelindex序号,在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勾选状态数组元素个数与dataArraysectionArray元素个数的关系,判断当前是否是全选状态并存入self.isSelectAll属性。遍历dataArray中的每一个sectionArray,接着遍历sectionArray中的每一个Model,filter过滤掉未选中的Model,map操作每一个处于选中状态的Model,计算该商品模型model的总金额并以@()数组元素的形式返回到数组之中,通过RACarray方法将序列转变成存满金额的数组,最后遍历这个数组对所有元素进行相加,如此得到的购物车被选中所有商品的总共金额并存入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被点击:传递cellindexPathbuttotn.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_textSignalTextField开始编辑开始,只要字符变化一下,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属性值与Colorbutton.selected的绑定,这都是套路,订阅RACObserve(self, number)中number属性值的变化,减号button.selected = @(number>1),加号button.selected = @(number<库存)RAC(TextField,textColor) = 库存?[UIColor blackColor]:[UIColor redColor];最后,有很重要的一点就是需要将Block回掉到cell的单个商品数量同时保存在cell.view.number属性里,因为数据的刷新还得是依靠CellreloadData

    • 单个商品数量变化的深层次逻辑。当单个商品数量通过Block回调到Cell之后,cell是不能负责因为当个商品数量变化引起的控制器逻辑变化,必须继续把当个商品数量和CellIndexPath以参数的形式传递到ViewModel中进行处理。逻辑包括:根据IndexPath定位CellModel,修改重置Model的单个商品数量属性,修改模型后就,可以reloadSection改组的所有Cell数据,最后重新计算当前所有被选中商品的总金额。

你可能感兴趣的:(电商购物(MVVM+ReactiveCocoa))