在上一篇介绍 MCCS:一种全新的 iOS APP 构建方式中,我们介绍了什么是 MCCS。MCCS 是一种设计模式,它的具体实现是 MCCSframework。
MCCSframework 当前只有 O-C 语言的版本,目前还没有 Swift 版本。
MCCSframework 的目前版本是 0.5.1,它的地址是:https://gitee.com/kmyhy/MCCSframework。
你可以直接下载它,也可以用 CocoaPods 来安装。它目前提供了 framework (二进制方式),没有源代码。
用 CocoaPods 安装非常简单,这是我们推荐的安装方式。编辑你的 Podfile 文件,添加 :
pod 'MCCSframework','~>0.5.1'
然后 pod install。注意,MCCSframework 依赖了一些第三方库,pod install 后会自动在你的项目中安装它们。这等同于在你的 Podfile 文件中增加了以下内容:
pod 'IGListKit', '~> 2.0.0'
pod 'UICollectionViewLeftAlignedLayout'
pod 'AFNetworking'
pod 'MBProgressHUD', '~> 0.9.2'
pod 'JSONModel'
pod 'Masonry'
pod 'ReactiveObjC', '~> 3.0.0' #RAC
pod 'MJRefresh'
此外,还需要修改项目的 Building Settings,将 Enable Bitcode 设置为 NO,将 Allow Non-modular … 设置为 YES。
设置完后,就可以在你的项目中使用 MCCSframework 了。
安装完成后,就可以在项目中使用 MCCS 架构的方式来编写 APP 了。注意,MCCS 和 MVC 是完全无缝兼容的,它不需要你破坏原来的项目结构和代码编写方式。你完全可以在项目中同时使用 MVC 和 MCCS。将一些简单的界面仍然使用 MVC 方式构建,而复杂界面则使用 MCCS 构建。
当然,为了演示,我们可以用一些不是那么“复杂”的界面来开始。
假设我们要实现这样一个界面:
依据前面提过的原则,我们可以将它划分几种不同的 cell,如下图所示:
其中:
这样在这个 UI 中将会涉及到 4 个 UICollectionViewCell 类。划分完 cell,接下来我们来看看这些 cell 需要由几个子控制器来进行管理。
因为子控制器实际上负责替 ViewController 管理屏幕,每个子控制器分别控制器屏幕的一片区域,从视图可知,整个屏幕被我们从上到下划分成了 4 段,因此我们需要 4 个子控制器。
在这里需要注意:
因此,整个界面需要用到 4 个 Cell 类、3 个子控制器类。
显示列表标题的 cell 我们可以使用框架提供的 OneLabelCell 类。
这个 cell 只包含了一个 UILabel 的属性 lbTitle。
二级分类的 cell 可以用框架提供的 EmbedCollectionViewCell 类。这个 cell 中嵌套了一个 UICollectionView。
二级分类的 cell 中嵌套的 UICollectionView 需要使用一种 cell,我们需要创建 Collection View 自身所用到的 cell。
新建一个 UICollectionViewCell,名为 SecondCategoryCell,勾选 Alson create XIB File。
打开 SecondCategoryCell,拖入一个 UIImageView 和一个 UILabel:
创建两个 IBOutlet:
#import
@interface SecondCategoryCell : NibCollectionViewCell
@property (weak, nonatomic) IBOutlet UIImageView *ivImage;
@property (weak, nonatomic) IBOutlet UILabel *lbTitle;
@end
注意,这里继承了框架提供的 NibCollectionViewCell。NibCollectionViewCell 提供了从 xib 文件中实例化一个 UICollectionViewCell 的能力。
商品列表所用 cell 为 OptimumGoodsCell。OptimumGoodsCell.xib 的内容如下,包含了一个 UIImageView 和 3 个 Label:
OptimumGoodsCell 中创建的 IBOutlet 如下:
#import
@interface OptimumGoodsCell : NibCollectionViewCell
@property (weak, nonatomic) IBOutlet UILabel *lbPurchaser;
@property (weak, nonatomic) IBOutlet UILabel *lbPrice;
@property (weak, nonatomic) IBOutlet UIView *shadowView;
@property (weak, nonatomic) IBOutlet UILabel *lbName;
@property (weak, nonatomic) IBOutlet UIImageView *ivImage;
@end
注意,这里实现了框架提供的 Configurable 接口。该接口需要实现一个 configWithObject:(id)obj 方法。OptimumGoodsCell 实现了这个方法:
#import "OptimumGoodsCell.h"
#import "Goods.h"
#import
#import
#import
@implementation OptimumGoodsCell
-(void)configWithObject:(id)obj{
if([obj isKindOfClass:Goods.class]){
Goods* goods = (Goods*)obj;
// 价格
self.lbPrice.text = f2s(goods.price,2);
// 销量
self.lbPurchaser.text = [NSString stringWithFormat:@"销量 %ld",goods.salesVolume];
// 商品名称
self.lbName.text = goods.productName;
// 商品图片
[self.ivImage sd_setImageWithURL:remoteImgAddr(goods.mainImgId) placeholderImage:[UIImage imageNamed:@"商品"]
];
}
}
cell 就创建好了,接下来是子控制器。
前面讨论到的 3 个子控制器类分别是:
OneLabelSC.h:
#import
@interface OneLabelSC : SubController
@property (strong, nonatomic) NSString* title;
@property (strong, nonatomic) UIFont* font;
@property (strong, nonatomic) UIColor* color;
@property(assign, nonatomic) CGFloat height;
@property(assign, nonatomic) CGFloat leading;
@end
OneLabelSC.m:
```
#import
#import
#import
@implementation OneLabelSC
// 1
-(instancetype)init{
if(self = [super init]){
self.font = [UIFont boldSystemFontOfSize:15];
self.color = hex_color(0x333333);
self.height = 50;
self.leading = 20;
}
return self;
}
// 2
- (NSInteger)numberOfItems{
return 1;
}
// 3
- (CGSize)sizeForItemAtIndex:(NSInteger)index{
return CGSizeMake(SCREEN_WIDTH, _height);
}
// 4
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
OneLabelCell* cell = [self.collectionContext dequeueReusableCellOfClass:[OneLabelCell class] forSectionController:self atIndex:index];// Crash!!! [MultilineTextInputCell new];
cell.lbTitle.text = _title;
cell.lbTitle.textColor = _color;
cell.lbTitle.font = _font;
cell.lbTitleLeading.constant = 20;
return cell;
}
@end
```
继承 SubController,需要覆盖 SubController 的 5 个方法。这 5 个方法中,只有 3 个方法必须实现。这些方法在上一篇文章中已经介绍过。这里我们只覆盖了其中 3 个。
SecondCategorySC.h:
#import "GoodsType.h"
#import
@interface SecondCategorySC : SubController
@property (strong, nonatomic) GoodsType* goodsType; // 1
@property (strong, nonatomic) void(^subclassSelected)(TypeNode* subclass);// 2
@end
SecondCategorySC.m:
#import
#import
#import "SecondCategoryCell.h"
#import
#import
#import
#import
#import
#import "UIViewController+Navigation.h"
#import
@interface SecondCategorySC()
@end
@implementation SecondCategorySC
- (NSInteger)numberOfItems{
return 1;
}
- (CGSize)sizeForItemAtIndex:(NSInteger)index{
return CGSizeMake(SCREEN_WIDTH, 110);
}
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
EmbedCollectionViewCell* cell = [self.collectionContext dequeueReusableCellOfClass:EmbedCollectionViewCell.class forSectionController:self atIndex:index];
[cell.collectionView registerNib:[UINib nibWithNibName:@"SecondCategoryCell" bundle:nil] forCellWithReuseIdentifier:@"SecondCategoryCell"];
cell.collectionView.delegate = self;
cell.collectionView.dataSource = self;
[cell.collectionView reloadData];
cell.collectionViewLeading.constant = 16;
cell.collectionViewTrail.constant = 16;
cell.collectionView.layer.cornerRadius = 8;
return cell;
}
#pragma mark -----UICollectionViewDataSource-----
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
SecondCategoryCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"SecondCategoryCell" forIndexPath:indexPath];
TypeNode* node = _goodsType.children[indexPath.row];
if(!stringIsEmpty(node.icon)){
[cell.ivImage sd_setImageWithURL:remoteImgAddr(node.icon) placeholderImage:[UIImage imageNamed:@"goodstype_placeholder"]];
}
cell.lbTitle.text = _goodsType.children[indexPath.row].categoryName;
return cell;
}
//每一组有多少个cell
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return _goodsType.children.count;
}
#pragma mark -----UICollectionViewDelegate-----
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
TypeNode* subclass = _goodsType.children[indexPath.item];
if(_subclassSelected){
_subclassSelected(subclass);
}
}
//定义每一个cell的大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
return CGSizeMake(114,110);
}
@end
这个子控制器的代码要比之前的多一些,因为这个子控制器除了要实现 SubController 的 3 个必须方法外,还额外实现了UICollectionViewDelegate 协议和 UICollectionViewDataSource 协议。
在 SubController 必须覆盖的 3 个方法中,前两个方法都简单,需要注意的仅仅是 cellForItemAtIndex 方法。在这个方法中:
所以接下来我们要实现UICollectionViewDelegate 协议和 UICollectionViewDataSource 协议。这部分内容和我们在使用 UIKit 中的做法没有什么两样,你应该很熟悉了。
OptimumGoodsSC 负责管理显示商品列表的 cell。类的声明如下:
#import "Goods.h"
#import
@interface OptimumGoodsSC : SubController
@property (strong, nonatomic) NSMutableArray* goodsArray;
@end
goodsArray 是一个模型数组,它的类型是 Goods 类型,用于表示商品的具体数据。这个类在“模型”一节介绍。
类的实现如下:
#import "OptimumGoodsSC.h"
#import "OptimumGoodsCell.h"
#import
@implementation OptimumGoodsSC
- (instancetype)init{
self = [super init];
if (self) {
self.inset=UIEdgeInsetsMake(0, 16, 0, 0);
self.minimumLineSpacing=5;
self.minimumInteritemSpacing=2;
}
return self;
}
-(NSMutableArray*)goodsArray{
if(!_goodsArray){
_goodsArray = [NSMutableArray new];
}
return _goodsArray;
}
- (NSInteger)numberOfItems{
return self.goodsArray ? self.goodsArray.count: 0;
}
- (CGSize)sizeForItemAtIndex:(NSInteger)index{
return CGSizeMake((SCREEN_WIDTH-32)/2,265);
}
-(UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
OptimumGoodsCell* cell = [self.collectionContext dequeueReusableCellOfClass:OptimumGoodsCell.class forSectionController:self atIndex:index];
[cell configWithObject:_goodsArray[_noSectionTitle ? index : index-1]];
return cell;
}
@end
大部分代码都是和之前的子控制器一样的套路,不同的在于:
在子控制器中,我们用到了2个模型类 Goods 和 GoodsType,这些模型和正常 APP 中使用的模型——相对于 MVC 中的模型——没有任何区别。
其实这个模型包含了 2 个类:
#import
@interface TypeNode : JKModel
@property (strong, nonatomic) NSString* categoryId;
@property (strong, nonatomic) NSString* categoryName;
@property (strong, nonatomic) NSString* description;
@property (strong, nonatomic) NSString* icon;
@end
@interface GoodsType : JKModel
@property (nonatomic,copy) TypeNode *parent;
@property(nonatomic,strong) NSArray *children;
@end
JKModel 是框架提供的,用于解析 JSON 格式的数据(使用了 JSONModel)。
这个模型也包含了 2 个类:
#import
#import "PageModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface Goods : JKModel
@property (strong, nonatomic) NSString* id;
@property (strong, nonatomic) NSString* productId;// 查询商品详情使用 productId 不要用 id
@property (strong, nonatomic) NSString* categoryId;
@property (strong, nonatomic) NSString* belongsShopId;
@property (strong, nonatomic) NSString* productName;
@property (strong, nonatomic) NSString* introduction;// 文字介绍
@property (strong, nonatomic) NSString* mainImgId;// 封面图片
@property (strong, nonatomic) NSString* imgId;// 展示图片 Id 列表
@property(assign, nonatomic) CGFloat price;// 价格
@property(assign, nonatomic) CGFloat discountPrice;// 打折价
@property (strong, nonatomic) NSString* priceUnit;// 单位
@property(assign, nonatomic) NSInteger inventory;// 库存
@property(assign, nonatomic) NSInteger salesVolume;// 销量
@property (strong, nonatomic) NSString* shipMethod;// 配送方式 1 线下自提
@property (strong, nonatomic) NSString* remarks;//
//@property (strong, nonatomic) NSString* amount;// 查我的订单时会有该字段
// 非实体映射属性
@property(assign, nonatomic) NSInteger amount;
@property(assign, nonatomic) BOOL checked;
@end
@interface GoodsPage : PageModel
@property (strong, nonatomic) NSArray* records;
@end
经常使用 JSONModel 的人应该非常熟悉这些代码了,基本上是用工具生成的代码。其中 GoodsPage 是接口返回的分页数据,它主要包含了一个 Goods 数组,此外还包含了从父类 PageModel 继承来的一些属性,比如页码、页大小等。
当 cell、子控制器、模型这些原材料准备好之后,实现控制器是一件非常简单的过程。
控制器的类声明如下:
#import
#import "GoodsType.h"
@interface SecondCategoryVC : NavBarVC // 1
@property (strong, nonatomic) GoodsType* goodsType; // 2
@end
在 SecondCategoryVC.m 中,我们首先声明两个属性:
@interface SecondCategoryVC()
@property (strong, nonatomic) SecondCategorySC* subclassSC;// 二级分类列表
@property (strong, nonatomic) OptimumGoodsSC* optimumSC;// 商品列表
@end
分别对应二级分类子控制器和商品列表子控制器。然后以懒加载的方式初始化两个子控制器:
// MARK: - Lazy load
-(OptimumGoodsSC*)optimumSC{
if(!_optimumSC){
_optimumSC = [OptimumGoodsSC new];
}
return _optimumSC;
}
-(SecondCategorySC*)subclassSC{
if(!_subclassSC){
_subclassSC = [SecondCategorySC new];
_subclassSC.goodsType = _goodsType;
@weakify(self)
_subclassSC.subclassSelected = ^(TypeNode * _Nonnull subclass) {
@strongify(self)
NSLog(@"用户选择了二级分类:%@",subclass.categoryName);
};
}
return _subclassSC;
}
注意,二级分类子控制器中,我们设置了 subclassSelected 块,这样当用户选中某个二级分类时,会打印这个二级分类的名称。
然后是 viewDidLoad 方法:
-(void)viewDidLoad{
[super viewDidLoad];
// 1
self.pageHeader.title = @"";
self.pageHeader.rightButtonHidden = YES;
// 2
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.pageHeader.mas_bottom);
make.left.mas_equalTo(0);
make.right.mas_equalTo(0);
make.bottom.mas_equalTo(0);
}];
// 下拉刷新
@weakify(self)
[self.collectionView addHeader:^{
@strongify(self)
[self.collectionView.mj_header endRefreshing];
[self loadFirstPage];
}footer:^{
@strongify(self)
[self.collectionView.mj_footer endRefreshing];
[self loadNextPage];
}];
// 3
[self addSC:[self labelSC:_goodsType.parent.categoryName]];
[self addSC:self.subclassSC];
[self addSC:[self labelSC:@"全部商品"]];
[self addSC:self.optimumSC];
[self.adapter reloadDataWithCompletion:nil];
// 4
[self loadFirstPage];
}
父类 NavBarVC 中有一个 pageHeader 属性,实际上就是一个导航条控件,我们需要设置它的标题,否则它会默认显示“安装进度”,隐藏它的 rightButton,否则默认会显示一颗“发布”按钮。
必须设置 CollectionView 的约束(使用 Masonry),否则 CollectionView 不会显示(默认 frame 为 0)。此外,我们还设置了 CollectionView 的上拉和下拉动作,以支持上拉加载和下拉刷新。这需要导入框架提供的 UIScrollView+addMJ.h 分类。
添加上面实现的 4 个子控制器。其中 labelSC: 方法会创建一个指定标题的单行标题文本(子控制器)。方法实现如下:
-(OneLabelSC*)labelSC:(NSString*)title{
OneLabelSC* _labelSC = [OneLabelSC new];
_labelSC.title = title;
return _labelSC;
}
当我们调用 addSC(添加子控制器)、rmSC(删除子控制器)、insSC(插入子控制器) 等方法后,一定不要忘记调用 adapter 的 reloadDataWithCompletion 方法。
调用 loadFirstPage 方法加载第一页数据。
在设置 CollectionView 的上拉和下拉 block 时,会用到这两个方法:
-(void)loadFirstPage{
self.collectionView.pageNum = 0;
[self loadNextPage];
}
-(void)loadNextPage{
if(self.collectionView.pageNum <=0){
[self.optimumSC.goodsArray removeAllObjects];
}
Goods* goods = [Goods new];
goods.belongsShopId =@"1";
goods.shipMethod=@"线下自提";
goods.id=@"84bd38cdb9b811e9acf6fa163e17a4af";
goods.salesVolume=0;
goods.productName=@"七彩洋芋";
goods.mainImgId=@"1565253738300";
goods.price=6.9000000000000004;
goods.discountPrice=5.1799999999999997;
goods.categoryId=@"2";
[self.optimumSC.goodsArray addObject:goods];
self.collectionView.pageNum++;
[self.optimumSC.collectionContext reloadSectionController:self.optimumSC];
}
分别用于下拉刷新和上拉加载。当然,现在都是 mock 的数据。后面我们会单独用一篇来介绍如何使用 MCCSframework 中的网络模块来加载真实的网络数据。作为演示,这里使用模拟数据就可以了。
还有一个工作,就是如何呈现 SecondCategoryVC。你可以用 presentViewController,也可以用 NavigationController PUSH。假设是后者,那么代码可能是这样的:
GoodsType* goodsType = [GoodsType new];
goodsType.parent = [TypeNode new];
goodsType.parent.categoryName = @"蔬菜";
goodsType.parent.icon = @"1564740275739";
TypeNode *child = [TypeNode new];
child.categoryName = @"根茎类";
child.icon = @"1564740355725";
goodsType.children =@[child];
[self pushSecondCagtegory:goodsType];
这里的 goodsType 仍然是 mock 模拟数据。运行程序,效果如下图所示:
上拉加载也没任何问题:
注意,你可能注意到,当你用自己的代码运行时,cell 中的所有图片(二级分类、商品图片)都不会显示。这并不是 MCCSframework 自身的问题。而是这些图片资源在源代码中并没有提供。如果你仅仅是想看一下效果,那么请搜索 sd_setImageWithURL 关键字,查到到这些地方:
[self.ivImage sd_setImageWithURL:remoteImgAddr(goods.mainImgId) placeholderImage:[UIImage imageNamed:@"商品"]
将 URL 参数从 remoteImgAddr 函数替换为任何你的 APP 能找到的图片资源。
关于 MCCSframework 的第一篇介绍就到此结束了。相信你已经发现了,教程中所用的例子已“接近于”真实案例,许多问题在生产中也是同样存在的。同时框架也提供了大量的实用函数、工具类和分类,这样在真实项目中很多时候程序员只需要编写业务代码即可。