【iOS】UICollectionView

【iOS】UICollectionView

前言

UICollectionView是iOS26之后UIKit框架中引入的一个新的控件,用于实现网格布局、横向滚动列表、照片墙、轮播图等复杂布局的强大控件。

相比UITableView:

  • 不再只能垂直滚动,其滚动方向可垂直、水平或自定义。
  • 通过UICollectionViewLayout控制进行布局。
  • 通过layout布局回调的代理方法可以动态地定制每个item的大小和collection的布局属性。
  • UICollectionView中Item的大小位置可自定义。
  • 更加灵活。

基础使用

我们先实现一个简单的九宫格布局:

  • 创建布局类并为其各个属性赋值
objc 复制代码
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
//设置布局方向为垂直流分布
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
//行间距
layout.minimumLineSpacing = 50;
//列间距
layout.minimumInteritemSpacing = 10;
//设置每个item的大小
layout.itemSize = CGSizeMake(100, 100);
  • 将自定义的布局类作为参数传递给UICollectionView的初始化
objc 复制代码
UICollectionView *collect = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:layout];
  • 设置代理
objc 复制代码
collect.delegate = self;
collect.dataSource = self;
  • 注册item类型
objc 复制代码
[collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
  • 实现协议函数

与tableView类似。

objc 复制代码
-(__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    //arc4random():C语言标准库函数,随机生成数
    //arc4random() % 255 / 255.0:使得随机生成在0-1之间,避免特别大的数
    cell.backgroundColor = [UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 255 / 255.0 alpha:1];
    return cell;
}

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return 9;
}

对比一下collectionView和tableView在复用cell时的区别:

  • UITableViewCell:

    • 旧式方法:如果复用池中没有cell,可以返回nil,再临时创建即可。
    objc 复制代码
    - (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
    objc 复制代码
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    • 新式方法:必须提前注册过cell。如果没有注册,系统会直接崩溃。
    objc 复制代码
    - (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath
  • UICollectionView:
    因为UICollectionView没有默认的cell类型,所以所有的cell都必须由开发者自定义并提前注册。

objc 复制代码
-(__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    return cell;
}

这样我们就实现了这个九宫格:

常用方法和属性

UICollectionViewFlowLayout属性

查看一下API,按照系统给我们的可以设置的属性依次有行间距、列间距、每个item的大小、预估大小、滚动方向、每个section头视图的尺寸、每个section尾视图的尺寸、每个section的内边距、sectionInset 的参考边界、header是否悬停、footer是否吸底。

其中常用的有:

  • 布局类方向

    • 垂直排布
    objc 复制代码
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
    • 水平排布
    objc 复制代码
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  • 设置item

objc 复制代码
//item大小
layout.itemSize = CGSizeMake(100, 100);
//item行间距
layout.minimumLineSpacing = 50;
//item列间距
layout.minimumInteritemSpacing = 10;
  • section的内边距
objc 复制代码
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

每个参数分别对应:

UICollectionView属性

  • 刷新对应列
objc 复制代码
[collect performBatchUpdates:^{
  [collect reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:nil];

协议代理方法

UICollectionViewDelegate

该协议中所有方法都是可选实现。

  • 是否允许某个item被高亮,按下item后,系统准备高亮显示前调用,默认YES会进入高亮状态(按下前)
objc 复制代码
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
  • 按下item后立即调用,此时手指还未松开(按下后)
objc 复制代码
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
  • 手指离开item区域(松开后)
objc 复制代码
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath;
  • 是否允许选中某个item,在item被选中前调用,默认YES允许选中(选中前)
objc 复制代码
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;
  • 是否允许取消选中,当再次点击已选中的item,系统准备取消选中时调用,默认YES允许取消选中(取消选中前)
objc 复制代码
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath; 

注意:该方法必须在允许多选时才可调用

objc 复制代码
collect.allowsMultipleSelection = YES;
  • 通知某个item已被选中,点击item后松手时调用,即真正选中时(选中后)
objc 复制代码
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
  • 通知某个item已被取消选中,当取消选中某个item时调用(取消选中后)
objc 复制代码
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;

UICollectionViewDataSource

查看一下API:

我们可以很清楚地看到以下两个方法是必须实现的:

  • 设置每个section的item数量
objc 复制代码
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
  • 返回对应位置的cell对象
objc 复制代码
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;

可选实现的:

  • 设置section数量,默认有1个section
objc 复制代码
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
  • 对头视图或尾视图进行设置
objc 复制代码
//UICollectionElementKindSectionHeader头视图
//UICollectionElementKindSectionFooter尾视图
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
  • 某个item是否可以被移动,默认返回NO
objc 复制代码
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(9.0));
  • 移动item时调用,只能在上述方法返回YES时调用
objc 复制代码
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath API_AVAILABLE(ios(9.0));

UICollectionViewDelegateFlowLayout

有时我们想让每个item展现不同,我们可以通过该协议方法设置。

根据API中方法依次可以设置每个item大小、每个section的内边距、行间距、列间距、头视图大小、尾视图大小。

实现瀑布流

实现原理

首先,我们要知道UICollectionView的布局过程实际上是调用UICollectionViewLayout的一些函数,这些函数的执行是依据一定顺序的。

简单来说,UICollectionView的布局是由UICollectionViewLayout的子类控制的。

因此,为了实现我们的自动移瀑布流,我们需要在其子类中重写生成布局的方法,创建我们自己需要的布局。

按照顺序实现以下方法:

  • 布局准备阶段调用
objc 复制代码
-(void)prepareLayout;
  • 设置collectionView内容区域总大小
objc 复制代码
-(CGSize)collectionViewContentSize;
  • 返回rect中所有元素的布局属性,返回一个数组
objc 复制代码
-(NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
  • 返回对应indexPath位置的item的布局属性
objc 复制代码
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
  • 返回区域头或区域尾的布局属性
objc 复制代码
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
  • 返回装饰视图的布局属性
objc 复制代码
- (nullable UICollectionViewLayoutAttributes )layoutAttributesForDecorationViewOfKind:(NSString)elementKind atIndexPath:(NSIndexPath *)indexPath;
  • 当边界发生改变时,是否刷新
objc 复制代码
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

实现步骤

  • -(void)prepareLayout

该方法在布局发生变化之前调用,比如初始化、调用reloadData、invalidateLayout(重新计算item位置和尺寸)等方法。用来进行布局前的准备,其主要作用有:

  1. 初始化布局参数:进行初始化工作,如计算和缓存用于布局的参数,可能包括计算每个单元格的大小、计算行列的数量、初始化用于存储布局属性的数据结构等。
  2. 计算并布局属性:计算并缓存集合视图中所有单元格的布局属性(UICollectionViewLayoutAttributes)。这样使得集合视图需要显示或进行交互时,可以直接访问缓存的布局属性,而不需要在运行时动态计算。
objc 复制代码
-(instancetype)init {
    if (self = [super init]) {
        self.columnCount = 2;
        self.columnMargin = 10;
        self.rowMargin = 10;
        self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
    }
    return self;
}

-(void)prepareLayout {
    [super prepareLayout];
    self.attributeArray = [NSMutableArray array];
    self.columnHeights = [NSMutableArray array];
    
    for (int i = 0; i < self.columnCount; i++) {
        [self.columnHeights addObject:@(self.sectionInset.top)];
    }
    //寻找最短列,保证下一个item放在当前最短列下面,使得整体布局符合瀑布流自然排列
    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i < itemCount; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        CGFloat minColumnHeight = [self.columnHeights[0] floatValue];
        NSInteger minColumnIndex = 0;
        for (int j = 1; j < self.columnCount; j++) {
            CGFloat columnHeight = [self.columnHeights[j] floatValue];
            if (columnHeight < minColumnHeight) {
                minColumnHeight = columnHeight;
                minColumnIndex = j;
            }
        }
        
        CGFloat itemWidth = (CGRectGetWidth(self.collectionView.bounds) - self.sectionInset.left - self.sectionInset.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
        CGFloat itemHeight = arc4random_uniform(100) + 100;
        CGFloat itemX = self.sectionInset.left + minColumnIndex * (itemWidth + self.columnMargin);
        CGFloat itemY = minColumnHeight;
        UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        attributes.frame = CGRectMake(itemX, itemY, itemWidth, itemHeight);
        [self.attributeArray addObject:attributes];
        //每次更新那一列的最新底部高度
        //CGRectGetMaxY的计算 = y + height
        self.columnHeights[minColumnIndex] = @(CGRectGetMaxY(attributes.frame) + self.rowMargin);
    }
}

我们通过API看一下我们可以设置哪些布局类对象的属性:

  • -(CGSize)collectionViewContentSize

该方法在需要计算集合视图内容尺寸时调用,比如初始化时、布局发生变化时等。决定了集合视图可以滚动的范围,即集合视图可以在其内容区域内滚动的最大范围,返回整个集合视图内容的尺寸。

objc 复制代码
-(CGSize)collectionViewContentSize {
    CGFloat maxColumnHeight = [self.columnHeights[0] floatValue];
    for (int i = 1; i < self.columnCount; i++) {
        maxColumnHeight = MAX(maxColumnHeight, [self.columnHeights[i] floatValue]);
    }
    return CGSizeMake(self.collectionView.bounds.size.width, maxColumnHeight);
}
  • -(NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect

在需要显示或更新某个区域内视图时调用,比如滚动、布局发生变化时。传入一个rect参数,返回在给定rect区域内的所有视图的布局属性数组。

objc 复制代码
-(NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    return self.attributeArray;
}
  • 调用-(__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath,创建item,配置单元格。
objc 复制代码
-(__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"-(__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath");
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    //arc4random():C语言标准库函数,随机生成数
    //arc4random() % 255 / 255.0:使得随机生成在0-1之间,避免特别大的数
    cell.backgroundColor = [UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 255 / 255.0 alpha:1];
    return cell;
}

调用顺序

我们使用NSLog打印出来每个函数,可以直观的看出:-(void)prepareLayout方法只调用一次,之后会在往下滑动UICollectionView时不断重复调用其他方法。

实现效果

总结

相较于仅能纵向单列数据展示的tableView,collectionView允许我们进行更加自由灵活的布局。而自定义UICollectionViewFlowLayout类,且重写该类中方法可以实现我们单元格的多样布局。

相关推荐
传奇开心果编程17 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
xUxIAOrUIII18 小时前
日常笔记-1005-1
linux·笔记·python·macos
tink19 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架1 天前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
可乐鸡翅yeah_1 天前
hls.js 缓冲区参数 maxBufferLength、maxBufferSize 通俗讲解与业务调参
开发语言·javascript·ios·音视频·safari·m3u8