Objective-C 在 iOS 集合视图(UICollectionView)中的实践
UICollectionView 基础概述
UICollectionView 是什么
UICollectionView 是 iOS 开发中用于展示集合数据的视图,它提供了一种灵活且高度可定制的方式来展示各种布局的内容,例如图片集、商品列表等。与 UITableView 类似,UICollectionView 也是基于数据源和代理模式工作,但它在布局方面更加灵活。UITableView 主要是线性的列表布局,而 UICollectionView 可以实现瀑布流、网格布局等多种复杂布局。
UICollectionView 的组成部分
- 数据源(DataSource):负责提供展示的数据,就像 UITableViewDataSource 一样,UICollectionViewDataSource 协议定义了必须实现的方法,如返回集合视图中项目的数量,以及为每个项目提供对应的单元格。
- 代理(Delegate):处理与用户交互相关的逻辑,例如单元格的选中、高亮显示等。UICollectionViewDelegate 协议提供了一系列方法来处理这些交互。
- 布局(Layout):UICollectionViewFlowLayout 是 UICollectionView 自带的一种布局类,它可以实现网格、流水布局等常见布局。开发者也可以继承 UICollectionViewLayout 来自定义布局,实现非常独特的展示效果。
- 单元格(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
类,并指定了复用标识符。
实现数据源方法
- 返回项目数量:实现
collectionView:numberOfItemsInSection:
方法,该方法返回集合视图中指定分区的项目数量。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.dataArray.count;
}
- 提供单元格:实现
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
- 设置单元格大小:可以在
viewDidLoad
方法中设置UICollectionViewFlowLayout
的itemSize
属性来指定单元格的大小。
layout.itemSize = CGSizeMake(100, 100);
- 设置行间距和列间距:通过
minimumLineSpacing
和minimumInteritemSpacing
属性来设置行与行之间以及单元格与单元格之间的最小间距。
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
- 设置滚动方向:
UICollectionViewFlowLayout
支持水平和垂直滚动方向,可以通过scrollDirection
属性来设置。
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
自定义布局
- 继承 UICollectionViewLayout:创建一个新的类继承自
UICollectionViewLayout
,例如CustomCollectionViewLayout
。
#import <UIKit/UIKit.h>
@interface CustomCollectionViewLayout : UICollectionViewLayout
@end
- 重写布局方法:
- 准备布局:重写
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];
}
- 使用自定义布局:在视图控制器中使用自定义布局替换默认的
UICollectionViewFlowLayout
。
CustomCollectionViewLayout *customLayout = [[CustomCollectionViewLayout alloc] init];
self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:customLayout];
UICollectionView 的交互处理
单元格的选中与取消选中
- 实现代理方法:在视图控制器中实现
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:
方法中,当单元格取消选中时,将其背景颜色恢复为浅灰色。
长按手势处理
- 添加长按手势识别器:在
viewDidLoad
方法中为UICollectionView
添加长按手势识别器。
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
[longPressGesture setMinimumPressDuration:0.5];
[self.collectionView addGestureRecognizer:longPressGesture];
- 实现手势处理方法:
- (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 的性能优化
单元格复用
- 正确使用复用标识符:在注册单元格时,使用合适的复用标识符,确保在
collectionView:cellForItemAtIndexPath:
方法中通过复用队列获取单元格,避免频繁创建新的单元格。如前面的示例中,使用[collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
从复用队列中取出单元格。 - 减少单元格内视图的创建:如果单元格内有复杂的视图结构,尽量在单元格类的初始化方法中创建,而不是在
collectionView:cellForItemAtIndexPath:
方法中每次都重新创建。例如,可以在自定义的单元格类CustomCollectionViewCell
的initWithFrame:
方法中创建视图:
#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;
}
预取数据
- 实现预取协议:让视图控制器实现
UICollectionViewDataSourcePrefetching
协议。
@interface ViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching>
- 实现预取方法:
- (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]];
}
}
// 这里可以进行数据预加载操作,例如从网络请求数据
}
通过预取数据,可以提前准备好即将显示的单元格的数据,提高滚动的流畅性。
按需加载数据
- 分页加载:如果数据量较大,可以采用分页加载的方式。在视图控制器中添加一个属性来记录当前加载的页数。
@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 的动画效果
插入和删除动画
- 插入动画:当需要向集合视图中插入新的数据时,可以使用
insertItemsAtIndexPaths:
方法,并配合动画效果。
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:self.dataArray.count inSection:0];
[self.dataArray addObject:@"New Item"];
[self.collectionView insertItemsAtIndexPaths:@[newIndexPath]];
- 删除动画:删除数据时,使用
deleteItemsAtIndexPaths:
方法。
NSIndexPath *indexPathToDelete = [NSIndexPath indexPathForItem:0 inSection:0];
[self.dataArray removeObjectAtIndex:0];
[self.collectionView deleteItemsAtIndexPaths:@[indexPathToDelete]];
移动和变换动画
- 移动动画:通过
moveItemAtIndexPath:toIndexPath:
方法实现单元格的移动动画。
NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0];
NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:2 inSection:0];
[self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
- 变换动画:可以通过自定义布局属性的动画来实现变换动画。例如,在自定义布局类中重写
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
- 在导航栏中显示标题:在视图控制器的
viewDidLoad
方法中设置导航栏标题。
self.title = @"Collection View Example";
- 通过导航控制器进行页面跳转:可以在单元格的点击事件中,通过导航控制器跳转到其他视图控制器。例如,创建一个新的视图控制器
DetailViewController
,并在collectionView:didSelectItemAtIndexPath:
方法中进行跳转:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
DetailViewController *detailVC = [[DetailViewController alloc] init];
[self.navigationController pushViewController:detailVC animated:YES];
}
UICollectionView 与 UITabBarController
- 将 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 来展示丰富多样的内容。