GitHub: https://github.com/lll1024/JVShopcart
效果图
ViewController 通常是 iOS 项目中最“重”的文件,它常常会包含很多不必要的、本可以复用的、逻辑不清晰的代码。今天我们将会看到给 ViewController 瘦身的技术,让代码移动到合适的地方变得更加的可复用、逻辑性强、耦合性低。下面以购物车为例进行说明。
把 Data Source 和其他 Protocols 分离出来
不要用 self
作为 tableView
的代理,而是新建代理类将 UITableViewDataSource
的代码抽出来放到代理类中,就像下面这个代理类这样:
#import "JVShopcartTableViewProxy.h"
#import "JVShopcartBrandModel.h"
#import "JVShopcartCell.h"
#import "JVShopcartHeaderView.h"
@implementation JVShopcartTableViewProxy
#pragma mark UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.dataArray.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
JVShopcartBrandModel *brandModel = self.dataArray[section];
NSArray *productArray = brandModel.products;
return productArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
JVShopcartCell *cell = [tableView dequeueReusableCellWithIdentifier:@"JVShopcartCell"];
JVShopcartBrandModel *brandModel = self.dataArray[indexPath.section];
NSArray *productArray = brandModel.products;
if (productArray.count > indexPath.row) {
JVShopcartProductModel *productModel = productArray[indexPath.row];
NSString *productName = [NSString stringWithFormat:@"%@%@%@", brandModel.brandName, productModel.productStyle, productModel.productType];
NSString *productSize = [NSString stringWithFormat:@"W:%ld H:%ld D:%ld", productModel.specWidth, productModel.specHeight, productModel.specLength];
[cell configureShopcartCellWithProductURL:productModel.productPicUri productName:productName productSize:productSize productPrice:productModel.productPrice productCount:productModel.productQty productStock:productModel.productStocks productSelected:productModel.isSelected];
}
__weak __typeof(self) weakSelf = self;
cell.shopcartCellBlock = ^(BOOL isSelected){
if (weakSelf.shopcartProxyProductSelectBlock) {
weakSelf.shopcartProxyProductSelectBlock(isSelected, indexPath);
}
};
cell.shopcartCellEditBlock = ^(NSInteger count){
if (weakSelf.shopcartProxyChangeCountBlock) {
weakSelf.shopcartProxyChangeCountBlock(count, indexPath);
}
};
return cell;
}
#pragma mark UITableViewDelegate
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
JVShopcartHeaderView *shopcartHeaderView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"JVShopcartHeaderView"];
if (self.dataArray.count > section) {
JVShopcartBrandModel *brandModel = self.dataArray[section];
[shopcartHeaderView configureShopcartHeaderViewWithBrandName:brandModel.brandName brandSelect:brandModel.isSelected];
}
__weak __typeof(self) weakSelf = self;
shopcartHeaderView.shopcartHeaderViewBlock = ^(BOOL isSelected){
if (weakSelf.shopcartProxyBrandSelectBlock) {
weakSelf.shopcartProxyBrandSelectBlock(isSelected, section);
}
};
return shopcartHeaderView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 40;
}
- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@"删除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
if (self.shopcartProxyDeleteBlock) {
self.shopcartProxyDeleteBlock(indexPath);
}
}];
UITableViewRowAction *starAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"收藏" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
if (self.shopcartProxyStarBlock) {
self.shopcartProxyStarBlock(indexPath);
}
}];
return @[deleteAction, starAction];
}
@end
用单独的类处理业务逻辑
将所有的业务逻辑写在 ViewController
中是不可取的,这会导致逻辑混乱难以维护的困境,因为 ViewController
实质上担负的是个协调、控制和管理的角色。正确的流程应该是:View
收到交互事件告诉 ViewController
, 控制器调用处理类接口并拿到返回结果再 configure 相应的 View
, 就像下面这个控制器这样:
#import "JVShopcartViewController.h"
#import "JVShopcartTableViewProxy.h"
#import "JVShopcartBottomView.h"
#import "JVShopcartCell.h"
#import "JVShopcartHeaderView.h"
#import "JVShopcartFormat.h"
#import "Masonry.h"
@interface JVShopcartViewController ()<JVShopcartFormatDelegate>
@property (nonatomic, strong) UITableView *shopcartTableView; /**< 购物车列表 */
@property (nonatomic, strong) JVShopcartBottomView *shopcartBottomView; /**< 购物车底部视图 */
@property (nonatomic, strong) JVShopcartTableViewProxy *shopcartTableViewProxy; /**< tableView代理 */
@property (nonatomic, strong) JVShopcartFormat *shopcartFormat; /**< 负责购物车逻辑处理 */
@property (nonatomic, strong) UIButton *editButton; /**< 编辑按钮 */
@end
@implementation JVShopcartViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"购物车";
self.view.backgroundColor = [UIColor whiteColor];
[self addSubview];
[self layoutSubview];
[self requestShopcartListData];
}
- (void)requestShopcartListData {
[self.shopcartFormat requestShopcartProductList];
}
#pragma mark JVShopcartFormatDelegate
//数据请求成功回调
- (void)shopcartFormatRequestProductListDidSuccessWithArray:(NSMutableArray *)dataArray {
self.shopcartTableViewProxy.dataArray = dataArray;
[self.shopcartTableView reloadData];
}
//购物车视图需要更新时的统一回调
- (void)shopcartFormatAccountForTotalPrice:(float)totalPrice totalCount:(NSInteger)totalCount isAllSelected:(BOOL)isAllSelected {
[self.shopcartBottomView configureShopcartBottomViewWithTotalPrice:totalPrice totalCount:totalCount isAllselected:isAllSelected];
[self.shopcartTableView reloadData];
}
//点击结算按钮后的回调
- (void)shopcartFormatSettleForSelectedProducts:(NSArray *)selectedProducts {
}
//批量删除回调
- (void)shopcartFormatWillDeleteSelectedProducts:(NSArray *)selectedProducts {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:[NSString stringWithFormat:@"确认要删除这%ld个宝贝吗?", selectedProducts.count] preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self.shopcartFormat deleteSelectedProducts:selectedProducts];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
//全部删除回调
- (void)shopcartFormatHasDeleteAllProducts {
}
#pragma mark getters
- (UITableView *)shopcartTableView {
if (_shopcartTableView == nil){
_shopcartTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
_shopcartTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_shopcartTableView registerClass:[JVShopcartCell class] forCellReuseIdentifier:@"JVShopcartCell"];
[_shopcartTableView registerClass:[JVShopcartHeaderView class] forHeaderFooterViewReuseIdentifier:@"JVShopcartHeaderView"];
_shopcartTableView.showsVerticalScrollIndicator = NO;
_shopcartTableView.delegate = self.shopcartTableViewProxy;
_shopcartTableView.dataSource = self.shopcartTableViewProxy;
_shopcartTableView.rowHeight = 120;
_shopcartTableView.sectionFooterHeight = 10;
_shopcartTableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
_shopcartTableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
}
return _shopcartTableView;
}
- (JVShopcartTableViewProxy *)shopcartTableViewProxy {
if (_shopcartTableViewProxy == nil){
_shopcartTableViewProxy = [[JVShopcartTableViewProxy alloc] init];
__weak __typeof(self) weakSelf = self;
_shopcartTableViewProxy.shopcartProxyProductSelectBlock = ^(BOOL isSelected, NSIndexPath *indexPath){
[weakSelf.shopcartFormat selectProductAtIndexPath:indexPath isSelected:isSelected];
};
_shopcartTableViewProxy.shopcartProxyBrandSelectBlock = ^(BOOL isSelected, NSInteger section){
[weakSelf.shopcartFormat selectBrandAtSection:section isSelected:isSelected];
};
_shopcartTableViewProxy.shopcartProxyChangeCountBlock = ^(NSInteger count, NSIndexPath *indexPath){
[weakSelf.shopcartFormat changeCountAtIndexPath:indexPath count:count];
};
_shopcartTableViewProxy.shopcartProxyDeleteBlock = ^(NSIndexPath *indexPath){
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:@"确认要删除这个宝贝吗?" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[weakSelf.shopcartFormat deleteProductAtIndexPath:indexPath];
}]];
[weakSelf presentViewController:alert animated:YES completion:nil];
};
_shopcartTableViewProxy.shopcartProxyStarBlock = ^(NSIndexPath *indexPath){
[weakSelf.shopcartFormat starProductAtIndexPath:indexPath];
};
}
return _shopcartTableViewProxy;
}
- (JVShopcartBottomView *)shopcartBottomView {
if (_shopcartBottomView == nil){
_shopcartBottomView = [[JVShopcartBottomView alloc] init];
__weak __typeof(self) weakSelf = self;
_shopcartBottomView.shopcartBotttomViewAllSelectBlock = ^(BOOL isSelected){
[weakSelf.shopcartFormat selectAllProductWithStatus:isSelected];
};
_shopcartBottomView.shopcartBotttomViewSettleBlock = ^(){
[weakSelf.shopcartFormat settleSelectedProducts];
};
_shopcartBottomView.shopcartBotttomViewStarBlock = ^(){
[weakSelf.shopcartFormat starSelectedProducts];
};
_shopcartBottomView.shopcartBotttomViewDeleteBlock = ^(){
[weakSelf.shopcartFormat beginToDeleteSelectedProducts];
};
}
return _shopcartBottomView;
}
- (JVShopcartFormat *)shopcartFormat {
if (_shopcartFormat == nil){
_shopcartFormat = [[JVShopcartFormat alloc] init];
_shopcartFormat.delegate = self;
}
return _shopcartFormat;
}
- (UIButton *)editButton {
if (_editButton == nil){
_editButton = [UIButton buttonWithType:UIButtonTypeCustom];
_editButton.frame = CGRectMake(0, 0, 40, 40);
[_editButton setTitle:@"编辑" forState:UIControlStateNormal];
[_editButton setTitle:@"完成" forState:UIControlStateSelected];
[_editButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
_editButton.titleLabel.font = [UIFont systemFontOfSize:13];
[_editButton addTarget:self action:@selector(editButtonAction) forControlEvents:UIControlEventTouchUpInside];
}
return _editButton;
}
- (void)editButtonAction {
self.editButton.selected = !self.editButton.isSelected;
[self.shopcartBottomView changeShopcartBottomViewWithStatus:self.editButton.isSelected];
}
- (void)addSubview {
UIBarButtonItem *editBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.editButton];
self.navigationItem.rightBarButtonItem = editBarButtonItem;
[self.view addSubview:self.shopcartTableView];
[self.view addSubview:self.shopcartBottomView];
}
- (void)layoutSubview {
[self.shopcartTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.right.equalTo(self.view);
make.bottom.equalTo(self.shopcartBottomView.mas_top);
}];
[self.shopcartBottomView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.bottom.right.equalTo(self.view);
make.height.equalTo(@50);
}];
}
@end
处理类接口如下:
#import <Foundation/Foundation.h>
@protocol JVShopcartFormatDelegate <NSObject>
@required
- (void)shopcartFormatRequestProductListDidSuccessWithArray:(NSMutableArray *)dataArray;
- (void)shopcartFormatAccountForTotalPrice:(float)totalPrice
totalCount:(NSInteger)totalCount
isAllSelected:(BOOL)isAllSelected;
- (void)shopcartFormatSettleForSelectedProducts:(NSArray *)selectedProducts;
- (void)shopcartFormatWillDeleteSelectedProducts:(NSArray *)selectedProducts;
- (void)shopcartFormatHasDeleteAllProducts;
@end
@interface JVShopcartFormat : NSObject
@property (nonatomic, weak) id <JVShopcartFormatDelegate> delegate;
//请求购物车数据
- (void)requestShopcartProductList;
//选中/取消选中某个row
- (void)selectProductAtIndexPath:(NSIndexPath *)indexPath isSelected:(BOOL)isSelected;
//选中/取消选中某个section
- (void)selectBrandAtSection:(NSInteger)section isSelected:(BOOL)isSelected;
//改变商品数
- (void)changeCountAtIndexPath:(NSIndexPath *)indexPath count:(NSInteger)count;
//单个删除商品
- (void)deleteProductAtIndexPath:(NSIndexPath *)indexPath;
//批量删除商品
- (void)beginToDeleteSelectedProducts;
- (void)deleteSelectedProducts:(NSArray *)selectedArray;
//单个收藏商品
- (void)starProductAtIndexPath:(NSIndexPath *)indexPath;
//批量收藏商品
- (void)starSelectedProducts;
//全选 or 取消全选
- (void)selectAllProductWithStatus:(BOOL)isSelected;
//结算选中商品
- (void)settleSelectedProducts;
@end
购物车的组成:
-
JVShopcartViewController
: 购物车控制器 负责协调Model和View 只有100多行代码 -
JVShopcartFormat
: 负责网络请求与逻辑处理 -
JVShopcartTableViewProxy
: 作为控制器里边TableView的代理 -
View
: 包括Cell、HeaderView、CountView(改变商品数的视图)、BottomView(控制器底部包含结算按钮的视图) -
Model
: 包含BrandModel和ProductModel两层
- (void)shopcartFormatRequestProductListDidSuccessWithArray:(NSMutableArray *)dataArray;
- (void)shopcartFormatAccountForTotalPrice:(float)totalPrice
totalCount:(NSInteger)totalCount
isAllSelected:(BOOL)isAllSelected;
- (void)shopcartFormatSettleForSelectedProducts:(NSArray *)selectedProducts;
- (void)shopcartFormatHasDeleteAllProducts;
- 这是请求购物车列表成功之后的回调方法,将装有Model的数组回调到控制器;控制器将其赋给TableView的代理类
JVShopcartTableViewProxy
并刷新TableView。 - 这是用户在操作了单选、多选、全选、删除这些会改变底部结算视图里边的全选按钮状态、商品总价和商品数的统一回调方法,这条API会将用户操作之后的结果,也就是是否全选、商品总价和和商品总数回调给
JVShopcartViewController
, 控制器拿着这些数据调用底部结算视图BottomView的configure方法并刷新TableView,就完成了UI更新。 - 这是用户点击结算按钮的回调方法,这条API会将剔除了未选中ProductModel的模型数组回调给
JVShopcartViewController
,但并不改变原数据源因为用户随时可能返回。 - 这是用户删除了购物车所有数据之后的回调方法,你可能会做些视图的隐藏或者提示。
关于JVShopcartFormat
,这个类主要负责网络请求与逻辑处理以及结果的回调。下面依次介绍这些方法:
- (void)requestShopcartProductList;
- (void)selectProductAtIndexPath:(NSIndexPath *)indexPath isSelected:(BOOL)isSelected;
- (void)selectBrandAtSection:(NSInteger)section isSelected:(BOOL)isSelected;
- (void)changeCountAtIndexPath:(NSIndexPath *)indexPath count:(NSInteger)count;
- (void)deleteProductAtIndexPath:(NSIndexPath *)indexPath;
- (void)starProductAtIndexPath:(NSIndexPath *)indexPath;
- (void)selectAllProductWithStatus:(BOOL)isSelected;
- (void)settleSelectedProducts;
- 这是请求购物车数据源的方法,大家一般都是对AFNetworking进行二次封装来请求数据。
- 这是用户选中了某个产品或某个row的处理方法,因为这会改变底部结算视图所以一定会回调上文协议中的第二个方法, 下同。
- 这是用户选中了某个品牌或某个section的处理方法
- 这是用户改变了商品数量的处理方法
- 这是用户删除操作的处理方法
- 这是用户收藏操作的处理方法,这里没有回调任何方法,也可以根据需求添加回调方法。
- 这是用户全选操作的处理方法
- 这是用户结算操作的处理方法