【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类,且重写该类中方法可以实现我们单元格的多样布局。

相关推荐
qixingchao3 小时前
iOS Swift 线程开发指南
ios·swift
AirDroid_cn3 小时前
在 iOS 18 离线徒步地图,如何存储和调用?
ios
2501_915909063 小时前
iOS 发布 App 全流程指南,从签名打包到开心上架(Appuploader)跨平台免 Mac 上传实战
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_9159184118 小时前
HTTP抓包工具推荐,Fiddler使用教程、代理设置与调试技巧详解(含HTTPS配置与实战案例)
http·ios·小程序·https·fiddler·uni-app·webview
mjhcsp18 小时前
C++ 贪心算法(Greedy Algorithm)详解:从思想到实战
c++·ios·贪心算法
Digitally20 小时前
如何在iPhone 17/16/15上显示电池百分比
ios·cocoa·iphone
2501_915921431 天前
iOS 虚拟位置设置实战,多工具协同打造精准调试与场景模拟环境
android·ios·小程序·https·uni-app·iphone·webview
QuantumLeap丶1 天前
《Flutter全栈开发实战指南:从零到高级》- 11 -状态管理Provider
android·flutter·ios