iOS 用一个布局来解决嵌套问题—— UICollectionViewCompositionalLayout

4,628 阅读7分钟

一、实现目标

WechatIMG112.jpeg 当我们要实现App store的游戏页面的时候,惯性思维可能就是我们需要建立一个UITableView,并且在tableHeaderView或者在第一个cell内部嵌套一个横向滑动的UICollectionView
其实我们可以直接用一个collectionView就可以实现这么一个效果。这就是今天的主角——UICollectionViewCompositionalLayout

二、层级结构

141604-c6c374e85d4f8beb.webp
由图可见,层级关系为 NSCollectionLayoutItem -> NSCollectionLayoutGroup -> NSCollectionLayoutSection
由图片可知,相对于原来的layout,多了一个Group属性。

三、先了解一下前期需要的配置

1.NSCollectionLayoutSize

我们先看看如何去声明一个NSCollectionLayoutSize

//1.配置NSCollectionLayoutSize
NSCollectionLayoutDimension *itemSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:0.25];
NSCollectionLayoutDimension *itemSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:1.0];
NSCollectionLayoutSize *fractionalItemSize = [NSCollectionLayoutSize sizeWithWidthDimension:itemSizeWidthDimension
                                                                            heightDimension:itemSizeHeightDimension];

在这个函数中我们需要传入两个NSCollectionLayoutDimension对象,去分别控制Item的宽和高。我们点进去可以看到

+ (instancetype)fractionalWidthDimension:(CGFloat)fractionalWidth;
+ (instancetype)fractionalHeightDimension:(CGFloat)fractionalHeight;
+ (instancetype)absoluteDimension:(CGFloat)absoluteDimension;
+ (instancetype)estimatedDimension:(CGFloat)estimatedDimension;

相对于原来的UICollectionViewFlowLayout的定义方式。NSCollectionLayoutDimension提供了多种声明函数来定义。

  • fractionalWidthDimension && fractionalHeightDimension
    fractional是关键词,可以理解为一个相对布局,后面传入的是一个相对于父视图的一个比例值;
  • absoluteDimension
    absolute是关键词,可以理解为我们写frame的数值,你写了多少显示的就是多少;
  • estimatedDimension
    estimated这个单词我们再写UITableView的时候比较常见,顾名思义就是你可以给一个预估值;

2.NSCollectionLayoutItem

这里对象初始化函数中需要带上我们刚才生成的itemSize。

//2.配置NSCollectionLayoutItem
NSCollectionLayoutItem *item = [NSCollectionLayoutItem itemWithLayoutSize:fractionalItemSize];

3.NSCollectionLayoutGroup

一共提供了5个初始化的方法,主要分为3大类。

  • 水平
  • 垂直
  • 自定义

其中水平和垂直方向都提供了两种初始化的方式,主要的差别是多了一个count参数。
如果是自定义布局,需要传入一个NSCollectionLayoutGroupCustomItemProvider来决定这个 Group中Item的布局方式。通过Group可以在同一个Section中实现不同的布局方式。\

+ (instancetype)horizontalGroupWithLayoutSize:(NSCollectionLayoutSize*)layoutSize 
                             repeatingSubitem:(NSCollectionLayoutItem*)subitem 
                                        count:(NSInteger)count;
+ (instancetype)horizontalGroupWithLayoutSize:(NSCollectionLayoutSize*)layoutSize 
                                     subitems:(NSArray<NSCollectionLayoutItem*>*)subitems;
+ (instancetype)verticalGroupWithLayoutSize:(NSCollectionLayoutSize*)layoutSize 
                           repeatingSubitem:(NSCollectionLayoutItem*)subitem
                                      count:(NSInteger)count;
+ (instancetype)verticalGroupWithLayoutSize:(NSCollectionLayoutSize*)layoutSize
                                   subitems:(NSArray<NSCollectionLayoutItem*>*)subitems;
+ (instancetype)customGroupWithLayoutSize:(NSCollectionLayoutSize*)layoutSize 
                             itemProvider:(NSCollectionLayoutGroupCustomItemProvider)itemProvider;

需要注意的是:
这里初始化函数的第一个参数layoutSize需要重新声明一个size对象,不能使用我们第一步的那个size。

//3.配置NSCollectionLayoutGroup
NSCollectionLayoutDimension *gropSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:1.0];
NSCollectionLayoutDimension *gropSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:0.1];
NSCollectionLayoutSize *gropSize = [NSCollectionLayoutSize sizeWithWidthDimension:gropSizeWidthDimension
                                                                  heightDimension:gropSizeHeightDimension];
NSCollectionLayoutGroup *group = [NSCollectionLayoutGroup horizontalGroupWithLayoutSize:gropSize 
                                                                               subitems:@[item]];

4.NSCollectionLayoutSection

初始化的时候带上之前定义好的group就可以。

//4.配置NSCollectionLayoutSection
NSCollectionLayoutSection *section = [NSCollectionLayoutSection sectionWithGroup:group];

5.UICollectionViewCompositionalLayout

初始化的时候把我们之前声明好的section带入就可以

//5.配置UICollectionViewCompositionalLayout
UICollectionViewCompositionalLayout *layout = [[UICollectionViewCompositionalLayout alloc] initWithSection:section];

但是这样的声明方式只能声明一个section。


四、探索实际的应用

1.案例一:实现一个最简单的CompositionalLayout

上一个部分已经介绍了如何去实现一个CompositionalLayout,那么我们接下来就要把我们设计的布局放到CollectionView中去实现。只要我们在CollectionView的声明函数中代入我们的Layout或者单独的用collectionView.collectionViewLayout = layout;来实现就行。剩下的及时要去实现基本的代理方法就可以

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    

    //1.配置NSCollectionLayoutSize

    NSCollectionLayoutDimension *itemSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:0.25];

    NSCollectionLayoutDimension *itemSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:1.0];

    NSCollectionLayoutSize *fractionalItemSize = [NSCollectionLayoutSize sizeWithWidthDimension:itemSizeWidthDimension

                                                                                heightDimension:itemSizeHeightDimension];

    //2.配置NSCollectionLayoutItem

    NSCollectionLayoutItem *item = [NSCollectionLayoutItem itemWithLayoutSize:fractionalItemSize];

    //3.配置NSCollectionLayoutGroup

    NSCollectionLayoutDimension *gropSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:1.0];

    NSCollectionLayoutDimension *gropSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:0.1];

    NSCollectionLayoutSize *gropSize = [NSCollectionLayoutSize sizeWithWidthDimension:gropSizeWidthDimension

                                                                      heightDimension:gropSizeHeightDimension];

    NSCollectionLayoutGroup *group = [NSCollectionLayoutGroup horizontalGroupWithLayoutSize:gropSize

                                                                                   subitems:@[item]];
    //4.配置NSCollectionLayoutSection
    NSCollectionLayoutSection *section = [NSCollectionLayoutSection sectionWithGroup:group];
    //5.配置UICollectionViewCompositionalLayout
    UICollectionViewCompositionalLayout *layout = [[UICollectionViewCompositionalLayout alloc] initWithSection:section];
    UICollectionView *collectionView = [[UICollectionView alloc]initWithFrame:self.view.frame collectionViewLayout:layout];
    collectionView.delegate = self;
    collectionView.dataSource = self;
    [collectionView registerClass:[EazyCollectionViewCell class] forCellWithReuseIdentifier:NSStringFromClass([EazyCollectionViewCell class])];
    [self.view addSubview:collectionView];
}
#pragma mark - <UICollectionViewDelegate,UICollectionViewDataSource>
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 2;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 8;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    EazyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([EazyCollectionViewCell class]) forIndexPath:indexPath];
    if (indexPath.section ==0) {
        cell.backgroundColor = [UIColor redColor];
    } else {
        cell.backgroundColor = [UIColor yellowColor];
    }
    cell.titleLabel.text = [NSString stringWithFormat:@"%ld---%ld",(long)indexPath.section,(long)indexPath.item];
    return cell;
}

截屏2023-05-31 11.04.22.png
这就是我们实现的效果,直观的感觉是不是感觉和之前的Layout没什么区别,这么写反而更加的复杂。
这样的原因主要是因为我们的UICollectionViewCompositionalLayout的声明方式只绑定了一种NSCollectionLayoutSection

2.案例二:让UICollectionViewCompositionalLayout绑定多个section

让我们再次的点进UICollectionViewCompositionalLayout内部,我们能看到这么一个函数

- (instancetype)initWithSectionProvider:(UICollectionViewCompositionalLayoutSectionProvider)sectionProvider;

我们可以根据block中的sectionIndex属性来判断是哪个section从而提前布局

UICollectionViewCompositionalLayout *layout = [[UICollectionViewCompositionalLayout alloc] initWithSectionProvider:^NSCollectionLayoutSection * _Nullable(NSInteger sectionIndex, id<NSCollectionLayoutEnvironment>  _Nonnull layoutEnvironment) {

    

        if (sectionIndex == 0) {

            NSCollectionLayoutDimension *itemSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:0.25];

            NSCollectionLayoutDimension *itemSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:1.0];

            NSCollectionLayoutSize *fractionalItemSize = [NSCollectionLayoutSize sizeWithWidthDimension:itemSizeWidthDimension

                                                                                        heightDimension:itemSizeHeightDimension];

            

            NSCollectionLayoutItem *item = [NSCollectionLayoutItem itemWithLayoutSize:fractionalItemSize];

            NSCollectionLayoutDimension *gropSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:1.0];

            NSCollectionLayoutDimension *gropSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:0.1];

            NSCollectionLayoutSize *gropSize = [NSCollectionLayoutSize sizeWithWidthDimension:gropSizeWidthDimension

                                                                              heightDimension:gropSizeHeightDimension];

            NSCollectionLayoutGroup *group = [NSCollectionLayoutGroup horizontalGroupWithLayoutSize:gropSize

                                                                                           subitems:@[item]];

            NSCollectionLayoutSection *section = [NSCollectionLayoutSection sectionWithGroup:group];

            return section;

        } else {

            NSCollectionLayoutDimension *itemSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:0.5];

            NSCollectionLayoutDimension *itemSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:1.0];

            NSCollectionLayoutSize *fractionalItemSize = [NSCollectionLayoutSize sizeWithWidthDimension:itemSizeWidthDimension

                                                                                        heightDimension:itemSizeHeightDimension];

            

            NSCollectionLayoutItem *item = [NSCollectionLayoutItem itemWithLayoutSize:fractionalItemSize];

            NSCollectionLayoutDimension *gropSizeWidthDimension = [NSCollectionLayoutDimension fractionalWidthDimension:1.0];

            NSCollectionLayoutDimension *gropSizeHeightDimension = [NSCollectionLayoutDimension fractionalHeightDimension:0.2];

            NSCollectionLayoutSize *gropSize = [NSCollectionLayoutSize sizeWithWidthDimension:gropSizeWidthDimension

                                                                              heightDimension:gropSizeHeightDimension];

            NSCollectionLayoutGroup *group = [NSCollectionLayoutGroup horizontalGroupWithLayoutSize:gropSize

                                                                                           subitems:@[item]];

            NSCollectionLayoutSection *section = [NSCollectionLayoutSection sectionWithGroup:group];

           return section;

        }

    }];

截屏2023-05-31 11.22.51.png

3.进阶属性设置

3.1 间距

UICollectionViewFlowLayout设置间距属性只要设置minimumLineSpacingminimumInteritemSpacing就可以。
但是在UICollectionViewCompositionalLayout里面我暂时并没有发现这两个属性。这里设置间距主要分为三种:

  1. item和item之间
  2. group和group之间
  3. section和section之间

而设置间距的方式有两种方式:

  1. contentInsets
  2. spacing

3.1.1 ContentInsets

Item、Group 和 Section 都有一个属性 contentInsets 用于设置边距。

3.1.1.1 item.contentInsets
  • 当给 Item 设置contentInsets后的示意图:

灰色区域是 Item,红色框是 Item 的边界,红色的上下左右边距就是设置的 contentInsets

  • 设置语法
item.contentInsets = NSDirectionalEdgeInsetsMake(5, 5, 5, 5);
3.1.1.2 group.contentInsets
  • 当给 Group 设置contentInsets后的示意图:
    灰色区域是 Item,红色框是 Item 的边界,蓝色框是 Group 的边界,蓝色的上下左右边距就是设置的 contentInsets。
  • 设置语法
group.contentInsets = NSDirectionalEdgeInsetsMake(5, 5, 5, 5);
3.1.1.3 section.contentInsets
  • 当给 Section 设置contentInsets后的示意图:

    灰色区域是 Item,红色框是 Item 的边界,蓝色框是 Group 的边界,绿色框是 Section 的边界,绿色的上下左右边距就是设置的 contentInsets。

  • 设置语法

group.contentInsets = NSDirectionalEdgeInsetsMake(5, 5, 5, 5);

注意须知 为了使整体的上下左右边距一样,通常需要同时设置 Item 和 Group 的contentInsets


3.2 Spacing

可以直接给 Group 和 Section 设置相应的 Spacing 以达到设置 Item 和 Group 之间间距的目的,但这种需要精确计算间距的值,因为间距会挤占 Item 和 Group 的空间。

group.interItemSpacing = [NSCollectionLayoutSpacing fixedSpacing:8];
group.edgeSpacing = [NSCollectionLayoutEdgeSpacing spacingForLeading:[NSCollectionLayoutSpacing fixedSpacing:8] top:[NSCollectionLayoutSpacing fixedSpacing:8] trailing:[NSCollectionLayoutSpacing fixedSpacing:8] bottom:[NSCollectionLayoutSpacing fixedSpacing:8]];
section.interItemSpacing = [NSCollectionLayoutSpacing fixedSpacing:8];

通过实验得知: 当我们给group设置属性的时候,作用是来实现item之间的spacing; 当给section设置属性的时候,作用是来实现group之间的spacing;

3.3 滚动方式

typedef NS_ENUM(NSInteger,UICollectionLayoutSectionOrthogonalScrollingBehavior) {
    // default behavior. Section will layout along main layout axis (i.e. configuration.scrollDirection)
    //默认行为。Section将沿着主布局轴(即configuration.scrollDirection)进行布局。
    UICollectionLayoutSectionOrthogonalScrollingBehaviorNone,
    // NOTE: For each of the remaining cases, the section content will layout orthogonal to the main layout axis (e.g. main layout axis == .vertical, section will scroll in .horizontal axis)
    // Standard scroll view behavior: UIScrollViewDecelerationRateNormal
    //注意:对于其余每种情况,section内容的布局将与主布局轴正交(例如,主布局轴== .垂直,section将在。水平轴上滚动)
    //滚动视图的标准行为:uiscrollviewdedeerationratnormal
    UICollectionLayoutSectionOrthogonalScrollingBehaviorContinuous,
    // Scrolling will come to rest on the leading edge of a group boundary
    //滚动将停在组边界的前缘
    UICollectionLayoutSectionOrthogonalScrollingBehaviorContinuousGroupLeadingBoundary,
    // Standard scroll view paging behavior (UIScrollViewDecelerationRateFast) with page size == extent of the collection view's bounds
    //标准滚动视图分页行为(uiscrollviewdedeerationratefast),页面大小==集合视图边界的范围
    UICollectionLayoutSectionOrthogonalScrollingBehaviorPaging,
    // Fractional size paging behavior determined by the sections layout group's dimension
    //分段大小的分页行为由section布局组的维度决定
    UICollectionLayoutSectionOrthogonalScrollingBehaviorGroupPaging,
    // Same of group paging with additional leading and trailing content insets to center each group's contents along the orthogonal axis
    //与组分页相同,添加了额外的前导和尾内容插入,以使每个组的内容沿正交轴居中
    UICollectionLayoutSectionOrthogonalScrollingBehaviorGroupPagingCentered,
}

系统给我们提供了以上6种滚动方式,我们通过绑定多组不同的section的同时也可以给每个section设置不同的滚动方式,这样就可以实现我们这边文章的主题,
用一个布局来解决嵌套问题—— UICollectionViewCompositionalLayout

五、鸣谢

这次文章的撰写,我在网上也借鉴了很多优秀博主的文章:

  1. iOS开发之UICollectionViewCompositionalLayout
  2. CompositionalLayout_Demo

这两篇文章或者Demo中使用的都是Swift语言,虽然现在OC渐渐地已经不再那么火热,但是基于工作原因,使用的还是OC所以就萌生了写这么一篇文章的想法。也希望能在这个平台多和各位技术大牛们交流学习。