UIStackView 的應用 JVSFakeCollectionView

JVSFlipped發表於2018-08-09

前言

開發中,根據專案需求, 經常有需要在 UITableView 中巢狀 UICollectionView 的需求, 例如下圖

UIStackView 的應用 JVSFakeCollectionView

tableView 和 collectionView 都有一堆需要實現的代理方法, 不可否認 collectionView 的可定製化更高, 但是一些代理方法的頻繁呼叫有時候會造成負荷過高, 而且編碼的時候最好也能轉換思路, 多考慮其他的解決方案也是一種不錯的選擇.

解決方案

1.九宮格佈局

九宮格佈局是老生常談了, 這裡不再贅述, 網上有一堆堆的資料和程式碼. 需要的請自行百度.

2. UIStackView 的巢狀使用 ---- JVSFakeCollectionView

UIStackView 是 iOS9 開始蘋果提供的新的 UI 控制元件, 據說 iOS8 也能使用一些騷操作使用, 但是這裡不進行討論. 我們可以將一些控制元件丟進 StackView 裡, 再由 stackView 進行佈局.

UIStackView 為佈局一組控制元件提供了線性的佈局頁面,這組控制元件可以垂直顯示,也可以水平顯示。當 View 被加入到 UIStackView, 你不再需要為它設定約束。UIStackView 會自動管理子控制元件的佈局併為他們新增約束。這也就意味著,子控制元件可以去適應不同的螢幕尺寸。

JVSFakeCollectionView 很簡單, 其實就是將 StackView 巢狀起來使用, 簡單來說, 如下圖所示

UIStackView 的應用 JVSFakeCollectionView

廢話不多說, 直接上程式碼

為了完整展示, 我貼上了左右 .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 的好方法. 如果使用的時候遇到問題歡迎聯絡我