UIStackView 的应用 JVSFakeCollectionView

前言

开发中,根据项目需求, 经常有需要在 UITableView 中嵌套 UICollectionView 的需求, 例如下图

tableView 和 collectionView 都有一堆需要实现的代理方法, 不可否认 collectionView 的可定制化更高, 但是一些代理方法的频繁调用有时候会造成负荷过高, 而且编码的时候最好也能转换思路, 多考虑其他的解决方案也是一种不错的选择.

解决方案

1.九宫格布局

九宫格布局是老生常谈了, 这里不再赘述, 网上有一堆堆的资料和代码. 需要的请自行百度.

2. UIStackView 的嵌套使用 ---- JVSFakeCollectionView

UIStackView 是 iOS9 开始苹果提供的新的 UI 控件, 据说 iOS8 也能使用一些骚操作使用, 但是这里不进行讨论. 我们可以将一些控件丢进 StackView 里, 再由 stackView 进行布局.

UIStackView 为布局一组控件提供了线性的布局页面,这组控件可以垂直显示,也可以水平显示。当 View 被加入到 UIStackView, 你不再需要为它设置约束。UIStackView 会自动管理子控件的布局并为他们添加约束。这也就意味着,子控件可以去适应不同的屏幕尺寸。

JVSFakeCollectionView 很简单, 其实就是将 StackView 嵌套起来使用, 简单来说, 如下图所示

废话不多说, 直接上代码

为了完整展示, 我贴上了左右 .m 和 .h 文件 JVSFakeCollectionView.h

@class JVSFakeCollectionView;
@protocol JVSFakeCollectionViewSelectedDeleagate

- (void)didSelectedView:(UIView *)cell AtIndex:(NSInteger)index inView:(JVSFakeCollectionView *)fakeView;

@end
/**
 * 自动布局类似于 collectionView 的 View, 不过是使用 UIStackView 进行布局的
 * 可以先创建实例, 再通过已给的方法添加子控件
 */
@interface JVSFakeCollectionView : UIStackView

/**
 * view 的高度, 传入数据即可获得, 无需渲染.
 */
@property (nonatomic, assign, readonly) CGFloat fakeCollectionViewHeight;

/**
 * view 的总宽度, 传入数据即可获得, 无需渲染
 */
@property (nonatomic, assign, readonly) CGFloat fakeCollectionViewWidth;

/**
 * 子 view 的高度
 */
@property (nonatomic, assign, readonly) CGFloat cellHeight;

/**
 * 子 view 的宽度
 */
@property (nonatomic, assign, readonly) CGFloat cellWidth;

/**
 * 行数
 */
@property (nonatomic, assign, readonly) NSInteger rowNumber;

/**
 * 列数
 */
@property (nonatomic, assign, readonly) NSInteger columns;

/**
 * cell 的总个数(包含添加的空白cell)
 */
@property (nonatomic, assign, readonly) NSInteger cellCount;

/**
 * 实际 cell 的个数(不包含添加的空白 cell)
 */
@property (nonatomic, assign, readonly) NSInteger realCellCount;

/**
 * cell 数组(包含添加的空白cell)
 */
@property (nonatomic, strong, readonly) NSMutableArray *cellArrM;

/**
 * 实际 cell 数组(不包含添加的空白cell), 注意其实是副本.
 */
@property (nonatomic, strong, readonly) NSArray *realCellArrM;

/**
 * 行间距
 */
@property (nonatomic, assign, readonly) CGFloat rowSpacing;

/**
 * 列间距
 */
@property (nonatomic, assign, readonly) CGFloat columnSpacing;


/**
 * 点击代理
 */
@property (nonatomic, weak) NSObject <JVSFakeCollectionViewSelectedDeleagate> *delegate;


/**
 * 布局 fakeCollectionView, 需要提供 cell 的大小和每个 cell 的行间距和列间距, 会给创建出来的对象的 fakeCollectionViewHeight 属性和 fakeCollectionViewWidth 属性赋值, 可以用来在之后赋值size;

 @param subCells 子 View 的数组
 @param columnSpacing 列间距
 @param rowSpacing 行间距
 @param cellHeight cell 高度
 @param cellWidth cell 宽度
 @param columns 列数
 */
- (void)addSubViews:(NSMutableArray *)subCells
      ColumnSpacing:(CGFloat)columnSpacing
         RowSpacing:(CGFloat)rowSpacing
         CellHeight:(CGFloat)cellHeight
          CellWidth:(CGFloat)cellWidth
   NumeberOfColumns:(NSInteger)columns;


/**
 * 布局 fakeCollectionView 实例, 此方法要求提供 view 的高度和宽度, 以及子控件的高度和宽度, 间距会自己计算, 会给创建出来的对象的 fakeCollectionViewHeight 属性和 fakeCollectionViewWidth 属性赋值, 可以用来在之后赋值size, 注意此方法会先移除原先的子控件

 @param subCells 子控件数组
 @param viewWidth 总宽度
 @param viewHeight 总高度
 @param cellHeight 子控件高度
 @param cellWidth 子控件宽度
 @param columns 列数
 */
- (void)addSubViews:(NSMutableArray *)subCells
          ViewWidth:(CGFloat)viewWidth
         ViewHeight:(CGFloat)viewHeight
         CellHeight:(CGFloat)cellHeight
          CellWidth:(CGFloat)cellWidth
   NumeberOfColumns:(NSInteger)columns;

@end

复制代码

JVSFakeCollectionView.m

#import "JVSFakeCollectionView.h"

@interface JVSFakeCollectionView ()

/**
 * 缺少 cell 的个数, 为0代表刚好排满, 不需要补全
 */
@property (nonatomic, assign) NSInteger lessNum;

@property (nonatomic, strong) NSMutableArray *cellArrM;

@end

@implementation JVSFakeCollectionView

- (void)addSubViews:(NSMutableArray *)subCells
                                     ColumnSpacing:(CGFloat)columnSpacing
                                        RowSpacing:(CGFloat)rowSpacing
                                        CellHeight:(CGFloat)cellHeight
                                         CellWidth:(CGFloat)cellWidth
                                  NumeberOfColumns:(NSInteger)columns {
    if (0 == subCells.count) {
        return;
    }
    [self addSubViews:subCells
                                   ViewWidth:-1
                                  ViewHeight:-1
                                  CellHeight:cellHeight
                                   CellWidth:cellWidth
                               ColumnSpacing:columnSpacing
                                  RowSpacing:rowSpacing
                            NumeberOfColumns:columns];
}

- (void)addSubViews:(NSMutableArray *)subCells
          ViewWidth:(CGFloat)viewWidth
         ViewHeight:(CGFloat)viewHeight
         CellHeight:(CGFloat)cellHeight
          CellWidth:(CGFloat)cellWidth
   NumeberOfColumns:(NSInteger)columns {
    
    if (0 == subCells.count) {
        return ;
    }
    
    [self addSubViews:subCells
            ViewWidth:viewWidth
           ViewHeight:viewHeight
           CellHeight:cellHeight
            CellWidth:cellWidth
        ColumnSpacing:-1
           RowSpacing:-1
     NumeberOfColumns:columns];
}

- (void)addSubViews:(NSMutableArray *)subCells
                                 ViewWidth:(CGFloat)viewWidth
                                ViewHeight:(CGFloat)viewHeight
                                CellHeight:(CGFloat)cellHeight
                                 CellWidth:(CGFloat)cellWidth
                             ColumnSpacing:(CGFloat)columnSpacing
                                RowSpacing:(CGFloat)rowSpacing
                          NumeberOfColumns:(NSInteger)columns {
        // 如果有子控件, 先移除
        if (self.arrangedSubviews.count>0) {
            for (UIStackView *subview in self.arrangedSubviews) {
                [self removeArrangedSubview:subview];
                [subview removeFromSuperview];
            }
        }
        // 列数
        [self setValue:@(columns) forKey:@"columns"];
        // 行间距
        [self setValue:@(rowSpacing) forKey:@"rowSpacing"];
        // 列间距
        [self setValue:@(columnSpacing) forKey:@"columnSpacing"];
        // 子控件高度
        self.cellHeight = cellHeight;
        // 子控件宽度
        self.cellWidth = cellWidth;
        // 填充后每个子控件高度相等
        self.distribution = UIStackViewDistributionFillEqually;
        // 垂直排列
        self.axis = UILayoutConstraintAxisVertical;
        // 拉伸两边对齐
        self.alignment = UIStackViewAlignmentFill;
        // 子控件数组
        [self setValue:subCells forKey:@"cellArrM"];
        // 实际子控件个数
        [self setValue:@(subCells.count) forKey:@"realCellCount"];
        // 实际子控件数组, 注意其实是副本, 不能执行操作
        [self setValue:subCells.copy forKey:@"realCellArrM"];
        // 补全缺的子控件个数
        [self completeCells];
        // 填充后 Cell 个数
        [self setValue:@(subCells.count) forKey:@"cellCount"];
        // 行数目
        [self setValue:@(self.cellCount/self.columns) forKey:@"rowNumber"];
        // 设置行高
        [self setValue:@(viewHeight) forKey:@"fakeCollectionViewHeight"];
        [self setValue:@(viewWidth) forKey:@"fakeCollectionViewWidth"];
        // 设置间距
        [self setValue:@(rowSpacing) forKey:@"rowSpacing"];
        [self setValue:@(columnSpacing) forKey:@"columnSpacing"];
        // 行间距, 列间距均未设置
        if ((-1 == rowSpacing) && (-1 ==columnSpacing)) {
            [self setValue:@(((viewHeight - self.rowNumber*cellHeight)/(self.rowNumber-1))>0?((viewHeight - self.rowNumber*cellHeight)/(self.rowNumber-1)):0) forKey:@"rowSpacing"];
                [self setValue:@(((viewWidth - self.columns*cellWidth)/(self.columns-1))>0?((viewWidth - self.columns*cellWidth)/(self.columns-1)):0) forKey:@"columnSpacing"];
        }
        // 行间距
        self.spacing = self.rowSpacing;
        // 行高位设置
        if ((-1 == viewWidth) && (-1 == viewHeight)) {
            // 设置行高
            [self setValue:@(self.rowNumber*cellHeight+(self.rowNumber-1)*self.rowSpacing) forKey:@"fakeCollectionViewHeight"];
            [self setValue:@(self.columns*cellWidth+(self.columns-1)*self.columnSpacing) forKey:@"fakeCollectionViewWidth"];
        }
        // 添加各行 stackView
        [self addRowStackView];
}

/**
 * 补全子控件, 如果不需要补全, _lessNum 为0
 */
- (void)completeCells {
    self.lessNum = (self.realCellCount%self.columns==0)?0:(self.columns - self.realCellCount%self.columns);
    if (0 == self.lessNum) {
        return;
    }
    // 补全子控件
    for (NSInteger i = 0; i<self.lessNum; i++) {
        UIView *placeHoldView = [[UIView alloc] init];
        placeHoldView.backgroundColor = [UIColor clearColor];
        [self.cellArrM addObject:placeHoldView];
    }

}

/**
 * 添加每一行的 stackView
 */
- (void)addRowStackView {
    for (NSInteger i = 0; i<self.rowNumber; i++) {
        UIStackView *rowStackView = [self rowStackViewInRow:i inColumns:self.columns];
        [self addArrangedSubview:rowStackView];
    }
}
    

/**
 * 每一行的 stackView

 @param row 行
 @param colunms 列数
 @return stackView实例
 */
- (UIStackView *)rowStackViewInRow:(NSInteger)row
                          inColumns:(NSInteger)colunms {
    // 该行第一个的下标
    NSInteger firstIndex = row + row*(colunms-1);
    // 该行最后一个下标
    NSInteger lastIndex = firstIndex +(colunms-1);
    UIStackView *rowStackView = [[UIStackView alloc] init];
    rowStackView.distribution = UIStackViewDistributionFillEqually;
    rowStackView.axis = UILayoutConstraintAxisHorizontal;
    //列间距
    rowStackView.spacing = self.columnSpacing;
    rowStackView.alignment = UIStackViewAlignmentFill;
    for (NSInteger i = firstIndex; i<lastIndex+1; i++) {
        UIView *view = self.cellArrM[i];
        
        NSInteger index = row*colunms + i;
        view.tag = index;
        UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(didSelectedCell:)];
        recognizer.numberOfTapsRequired = 1;
        [view addGestureRecognizer:recognizer];

        
        [rowStackView addArrangedSubview:view];
    }
    return rowStackView;
}

- (void)setCellWidth:(CGFloat)cellWidth {
    _cellWidth = cellWidth;
}

- (void)setCellHeight:(CGFloat)cellHeight {
    _cellHeight = cellHeight;
}

- (void)didSelectedCell:(UITapGestureRecognizer *)recognizer {
    if ([self.delegate respondsToSelector:@selector(didSelectedView:AtIndex:inView:)]) {
        UIView *cell = recognizer.view;
        cell.userInteractionEnabled = YES;
        [self.delegate didSelectedView:cell AtIndex:cell.tag inView:self]; 
    }
    
}

@end
复制代码

基本上该有的注释代码里面都有, 我就不一一阐述了. 有以下几个注意点

  1. 可以看到有两种创建方式, 注释里面有介绍, 两个都是从左往右排列的, 如果你有些需求是要顶右边显示, 可以选择第一个方法, 他会自动调整自己的大小.
  2. 为了美观, 当一行不够布满的时候会自动补全透明的子控件.
  3. 代理是点击事件

后记

总的来说, 这个东西只是我用来代替 collectionView 的一个实验性的小轮子, 定制性和可靠性可能都比不上 collectionView, 但是不失为一个熟悉 StackView 的好方法. 如果使用的时候遇到问题欢迎联系我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值