MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C 在 iOS 集合视图(UICollectionView)中的实践

2022-07-011.2k 阅读

UICollectionView 基础概述

UICollectionView 是什么

UICollectionView 是 iOS 开发中用于展示集合数据的视图,它提供了一种灵活且高度可定制的方式来展示各种布局的内容,例如图片集、商品列表等。与 UITableView 类似,UICollectionView 也是基于数据源和代理模式工作,但它在布局方面更加灵活。UITableView 主要是线性的列表布局,而 UICollectionView 可以实现瀑布流、网格布局等多种复杂布局。

UICollectionView 的组成部分

  1. 数据源(DataSource):负责提供展示的数据,就像 UITableViewDataSource 一样,UICollectionViewDataSource 协议定义了必须实现的方法,如返回集合视图中项目的数量,以及为每个项目提供对应的单元格。
  2. 代理(Delegate):处理与用户交互相关的逻辑,例如单元格的选中、高亮显示等。UICollectionViewDelegate 协议提供了一系列方法来处理这些交互。
  3. 布局(Layout):UICollectionViewFlowLayout 是 UICollectionView 自带的一种布局类,它可以实现网格、流水布局等常见布局。开发者也可以继承 UICollectionViewLayout 来自定义布局,实现非常独特的展示效果。
  4. 单元格(Cell):UICollectionViewCell 是展示数据的基本单元,开发者可以自定义单元格的外观和行为,通过注册单元格类或 nib 文件,UICollectionView 可以复用单元格以提高性能。

Objective - C 中 UICollectionView 的基本设置

创建 UICollectionView

在 Objective - C 中,首先需要在视图控制器的头文件中声明一个 UICollectionView 实例变量:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate>

@property (nonatomic, strong) UICollectionView *collectionView;

@end

然后在实现文件的 viewDidLoad 方法中初始化 UICollectionView:

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化数据
    self.dataArray = @[@"Item1", @"Item2", @"Item3", @"Item4", @"Item5"];
    
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
    self.collectionView.dataSource = self;
    self.collectionView.delegate = self;
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
    [self.view addSubview:self.collectionView];
}

这里首先创建了一个 UICollectionViewFlowLayout 对象,然后使用该布局初始化 UICollectionView,并设置其数据源和代理为当前视图控制器。同时,注册了一个基本的 UICollectionViewCell 类,并指定了复用标识符。

实现数据源方法

  1. 返回项目数量:实现 collectionView:numberOfItemsInSection: 方法,该方法返回集合视图中指定分区的项目数量。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.dataArray.count;
}
  1. 提供单元格:实现 collectionView:cellForItemAtIndexPath: 方法,该方法为指定索引路径的项目提供对应的单元格。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor lightGrayColor];
    UILabel *label = [[UILabel alloc] initWithFrame:cell.contentView.bounds];
    label.text = self.dataArray[indexPath.item];
    label.textAlignment = NSTextAlignmentCenter;
    [cell.contentView addSubview:label];
    return cell;
}

在这个方法中,首先从复用队列中取出单元格,如果没有可复用的单元格,则会根据注册的类或 nib 文件创建新的单元格。然后设置单元格的背景颜色,并添加一个标签来显示数据。

UICollectionView 的布局定制

使用 UICollectionViewFlowLayout

  1. 设置单元格大小:可以在 viewDidLoad 方法中设置 UICollectionViewFlowLayoutitemSize 属性来指定单元格的大小。
layout.itemSize = CGSizeMake(100, 100);
  1. 设置行间距和列间距:通过 minimumLineSpacingminimumInteritemSpacing 属性来设置行与行之间以及单元格与单元格之间的最小间距。
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
  1. 设置滚动方向UICollectionViewFlowLayout 支持水平和垂直滚动方向,可以通过 scrollDirection 属性来设置。
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

自定义布局

  1. 继承 UICollectionViewLayout:创建一个新的类继承自 UICollectionViewLayout,例如 CustomCollectionViewLayout
#import <UIKit/UIKit.h>

@interface CustomCollectionViewLayout : UICollectionViewLayout

@end
  1. 重写布局方法
    • 准备布局:重写 prepareLayout 方法,在这个方法中进行布局的初始化和计算。
#import "CustomCollectionViewLayout.h"

@interface CustomCollectionViewLayout ()

@property (nonatomic, strong) NSMutableArray *layoutAttributesArray;

@end

@implementation CustomCollectionViewLayout

- (void)prepareLayout {
    [super prepareLayout];
    
    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    self.layoutAttributesArray = [NSMutableArray arrayWithCapacity:itemCount];
    
    CGFloat itemWidth = (self.collectionView.bounds.size.width - 20) / 3;
    CGFloat itemHeight = itemWidth;
    
    for (NSInteger i = 0; i < itemCount; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        CGFloat x = (i % 3) * (itemWidth + 10);
        CGFloat y = (i / 3) * (itemHeight + 10);
        attributes.frame = CGRectMake(x, y, itemWidth, itemHeight);
        [self.layoutAttributesArray addObject:attributes];
    }
}
- **返回布局属性**:重写 `layoutAttributesForElementsInRect:` 方法,该方法返回指定矩形区域内的所有单元格的布局属性。
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray *attributesArray = [NSMutableArray array];
    for (UICollectionViewLayoutAttributes *attributes in self.layoutAttributesArray) {
        if (CGRectIntersectsRect(rect, attributes.frame)) {
            [attributesArray addObject:attributes];
        }
    }
    return attributesArray;
}
- **返回特定单元格的布局属性**:重写 `layoutAttributesForItemAtIndexPath:` 方法,返回指定索引路径的单元格的布局属性。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return self.layoutAttributesArray[indexPath.item];
}
  1. 使用自定义布局:在视图控制器中使用自定义布局替换默认的 UICollectionViewFlowLayout
CustomCollectionViewLayout *customLayout = [[CustomCollectionViewLayout alloc] init];
self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:customLayout];

UICollectionView 的交互处理

单元格的选中与取消选中

  1. 实现代理方法:在视图控制器中实现 collectionView:didSelectItemAtIndexPath:collectionView:didDeselectItemAtIndexPath: 方法。
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    cell.backgroundColor = [UIColor redColor];
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    cell.backgroundColor = [UIColor lightGrayColor];
}

didSelectItemAtIndexPath: 方法中,当单元格被选中时,将其背景颜色设置为红色;在 didDeselectItemAtIndexPath: 方法中,当单元格取消选中时,将其背景颜色恢复为浅灰色。

长按手势处理

  1. 添加长按手势识别器:在 viewDidLoad 方法中为 UICollectionView 添加长按手势识别器。
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
[longPressGesture setMinimumPressDuration:0.5];
[self.collectionView addGestureRecognizer:longPressGesture];
  1. 实现手势处理方法
- (void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
        return;
    }
    
    CGPoint p = [gestureRecognizer locationInView:self.collectionView];
    NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:p];
    if (indexPath) {
        UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
        cell.backgroundColor = [UIColor greenColor];
    }
}

当长按手势开始且触摸点在某个单元格上时,将该单元格的背景颜色设置为绿色。

UICollectionView 的性能优化

单元格复用

  1. 正确使用复用标识符:在注册单元格时,使用合适的复用标识符,确保在 collectionView:cellForItemAtIndexPath: 方法中通过复用队列获取单元格,避免频繁创建新的单元格。如前面的示例中,使用 [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; 从复用队列中取出单元格。
  2. 减少单元格内视图的创建:如果单元格内有复杂的视图结构,尽量在单元格类的初始化方法中创建,而不是在 collectionView:cellForItemAtIndexPath: 方法中每次都重新创建。例如,可以在自定义的单元格类 CustomCollectionViewCellinitWithFrame: 方法中创建视图:
#import <UIKit/UIKit.h>

@interface CustomCollectionViewCell : UICollectionViewCell

@property (nonatomic, strong) UILabel *titleLabel;

@end

#import "CustomCollectionViewCell.h"

@implementation CustomCollectionViewCell

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.titleLabel = [[UILabel alloc] initWithFrame:self.contentView.bounds];
        self.titleLabel.textAlignment = NSTextAlignmentCenter;
        [self.contentView addSubview:self.titleLabel];
    }
    return self;
}

@end

然后在 collectionView:cellForItemAtIndexPath: 方法中只设置数据:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomCell" forIndexPath:indexPath];
    cell.titleLabel.text = self.dataArray[indexPath.item];
    return cell;
}

预取数据

  1. 实现预取协议:让视图控制器实现 UICollectionViewDataSourcePrefetching 协议。
@interface ViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching>
  1. 实现预取方法
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    NSMutableArray *itemsToFetch = [NSMutableArray array];
    for (NSIndexPath *indexPath in indexPaths) {
        if (indexPath.item < self.dataArray.count) {
            [itemsToFetch addObject:self.dataArray[indexPath.item]];
        }
    }
    // 这里可以进行数据预加载操作,例如从网络请求数据
}

通过预取数据,可以提前准备好即将显示的单元格的数据,提高滚动的流畅性。

按需加载数据

  1. 分页加载:如果数据量较大,可以采用分页加载的方式。在视图控制器中添加一个属性来记录当前加载的页数。
@property (nonatomic, NSInteger) currentPage;

viewDidLoad 方法中初始化当前页数:

self.currentPage = 1;

collectionView:numberOfItemsInSection: 方法中根据当前页数返回相应的数据数量:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return [self getDataForPage:self.currentPage].count;
}

其中 getDataForPage: 方法根据页数返回相应的数据数组。在滚动到接近底部时,加载下一页数据:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetY = scrollView.contentOffset.y;
    CGFloat contentHeight = scrollView.contentSize.height;
    CGFloat height = scrollView.frame.size.height;
    if (offsetY > contentHeight - height - 100) {
        self.currentPage++;
        // 加载下一页数据,并更新数据源
        [self.collectionView reloadData];
    }
}

UICollectionView 与数据模型

创建数据模型类

假设我们要展示一个商品列表,每个商品有名称、价格和图片。首先创建一个数据模型类 Product

#import <Foundation/Foundation.h>

@interface Product : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, NSNumber *price;
@property (nonatomic, UIImage *image;

- (instancetype)initWithName:(NSString *)name price:(NSNumber *)price image:(UIImage *)image;

@end

#import "Product.h"

@implementation Product

- (instancetype)initWithName:(NSString *)name price:(NSNumber *)price image:(UIImage *)image {
    self = [super init];
    if (self) {
        self.name = name;
        self.price = price;
        self.image = image;
    }
    return self;
}

@end

使用数据模型填充 UICollectionView

在视图控制器中,初始化一个包含 Product 对象的数组作为数据源:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSMutableArray *productArray = [NSMutableArray array];
    UIImage *image1 = [UIImage imageNamed:@"product1.jpg"];
    Product *product1 = [[Product alloc] initWithName:@"Product 1" price:@10.99 image:image1];
    [productArray addObject:product1];
    // 添加更多商品
    
    self.dataArray = productArray;
    
    // UICollectionView 设置
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
    self.collectionView.dataSource = self;
    self.collectionView.delegate = self;
    [self.collectionView registerClass:[CustomCollectionViewCell class] forCellWithReuseIdentifier:@"CustomCell"];
    [self.view addSubview:self.collectionView];
}

然后在 collectionView:cellForItemAtIndexPath: 方法中使用数据模型来设置单元格的内容:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomCell" forIndexPath:indexPath];
    Product *product = self.dataArray[indexPath.item];
    cell.titleLabel.text = product.name;
    // 设置价格和图片等其他内容
    return cell;
}

UICollectionView 的动画效果

插入和删除动画

  1. 插入动画:当需要向集合视图中插入新的数据时,可以使用 insertItemsAtIndexPaths: 方法,并配合动画效果。
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:self.dataArray.count inSection:0];
[self.dataArray addObject:@"New Item"];
[self.collectionView insertItemsAtIndexPaths:@[newIndexPath]];
  1. 删除动画:删除数据时,使用 deleteItemsAtIndexPaths: 方法。
NSIndexPath *indexPathToDelete = [NSIndexPath indexPathForItem:0 inSection:0];
[self.dataArray removeObjectAtIndex:0];
[self.collectionView deleteItemsAtIndexPaths:@[indexPathToDelete]];

移动和变换动画

  1. 移动动画:通过 moveItemAtIndexPath:toIndexPath: 方法实现单元格的移动动画。
NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0];
NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:2 inSection:0];
[self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
  1. 变换动画:可以通过自定义布局属性的动画来实现变换动画。例如,在自定义布局类中重写 prepareForCollectionViewUpdates: 方法,对布局属性进行修改,然后 UICollectionView 会自动为这些修改添加动画效果。
- (void)prepareForCollectionViewUpdates:(NSArray<UICollectionViewUpdateItem *> *)updateItems {
    [super prepareForCollectionViewUpdates:updateItems];
    
    for (UICollectionViewUpdateItem *updateItem in updateItems) {
        if (updateItem.updateAction == UICollectionViewUpdateActionDelete) {
            NSIndexPath *indexPath = updateItem.indexPathBeforeUpdate;
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
            attributes.alpha = 0.0;
        } else if (updateItem.updateAction == UICollectionViewUpdateActionInsert) {
            NSIndexPath *indexPath = updateItem.indexPathAfterUpdate;
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
            attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
        }
    }
}

UICollectionView 与其他视图的结合使用

UICollectionView 与 UINavigationController

  1. 在导航栏中显示标题:在视图控制器的 viewDidLoad 方法中设置导航栏标题。
self.title = @"Collection View Example";
  1. 通过导航控制器进行页面跳转:可以在单元格的点击事件中,通过导航控制器跳转到其他视图控制器。例如,创建一个新的视图控制器 DetailViewController,并在 collectionView:didSelectItemAtIndexPath: 方法中进行跳转:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    DetailViewController *detailVC = [[DetailViewController alloc] init];
    [self.navigationController pushViewController:detailVC animated:YES];
}

UICollectionView 与 UITabBarController

  1. 将 UICollectionView 所在视图控制器添加到标签栏控制器:在应用的根视图控制器设置中,将包含 UICollectionView 的视图控制器添加到 UITabBarController 的视图控制器数组中。
ViewController *viewController1 = [[ViewController alloc] init];
viewController1.title = @"Collection";
viewController1.tabBarItem.image = [UIImage imageNamed:@"collection_icon"];

AnotherViewController *viewController2 = [[AnotherViewController alloc] init];
viewController2.title = @"Other";
viewController2.tabBarItem.image = [UIImage imageNamed:@"other_icon"];

UITabBarController *tabBarController = [[UITabBarController alloc] init];
tabBarController.viewControllers = @[viewController1, viewController2];

这样,用户可以通过标签栏在包含 UICollectionView 的视图和其他视图之间切换。

通过以上内容,我们详细介绍了 Objective - C 中 UICollectionView 的各种实践,从基础设置、布局定制、交互处理到性能优化、与数据模型结合以及动画效果和与其他视图的结合使用,希望能帮助开发者更好地在 iOS 应用中使用 UICollectionView 来展示丰富多样的内容。