简介:在iOS开发中,UICollectionView通过自定义布局可实现如Pinterest风格的瀑布流效果。本资源“ios-collectionView实现瀑布流.zip”提供了一套完整的瀑布流实现代码,涵盖UICollectionViewFlowLayout子类自定义、图片异步加载与尺寸计算、数据源配置、Cell重用机制、布局刷新及性能优化等关键技术点。开发者可快速集成并适配数据源,打造流畅美观的瀑布流界面。
1. iOS开发中UICollectionView的基本概念与核心作用
UICollectionView 是iOS开发中用于展示集合数据的重要组件,相较 UITableView ,它提供了更灵活、更自由的布局能力。特别是在实现如瀑布流、网格布局、自由拖拽等复杂界面时, UICollectionView 展现出极大的优势。
其核心在于通过 UICollectionViewLayout 子类(如 UICollectionViewFlowLayout )实现多样化的布局结构。开发者可以通过自定义布局逻辑,动态控制每个Cell的位置、大小和动画效果,为构建高性能、高定制的UI打下基础。
2. UICollectionViewFlowLayout的创建与基础配置
在 iOS 开发中, UICollectionViewFlowLayout 是 UICollectionView 的默认布局方式,提供了类似于 UITableView 的线性布局能力,但其灵活性远超 UITableView 。通过 UICollectionViewFlowLayout ,开发者可以快速实现横向或纵向排列的列表布局,甚至在稍作修改后,可以实现更复杂的布局样式。本章将详细介绍 UICollectionViewFlowLayout 的基本结构、自定义类的创建步骤以及布局参数的配置与调试技巧。
2.1 UICollectionViewFlowLayout的基本结构
UICollectionViewFlowLayout 是 UICollectionViewLayout 的子类,负责定义 UICollectionView 中 item 的排列方式。其核心结构包括布局对象的生命周期管理、默认行为分析等,理解这些内容有助于后续自定义布局的实现。
2.1.1 布局对象的作用与生命周期
UICollectionViewFlowLayout 实例通常由 UICollectionView 自动创建,但也可以手动初始化并设置给 collectionView.collectionViewLayout 。其生命周期包括:
- 初始化 :通常通过
alloc和init创建,或使用UICollectionViewFlowLayout的便利构造方法。 - 注册与设置 :通过
collectionView.collectionViewLayout = flowLayout将布局对象绑定到collectionView。 - 布局计算 :当
collectionView需要显示内容时,会调用prepare()方法准备布局信息。 - 释放 :当
collectionView被销毁或布局对象被替换时,系统自动释放。
以下代码展示了如何创建并设置一个 UICollectionViewFlowLayout :
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical // 设置滚动方向为垂直
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
逐行解读:
- 创建一个
UICollectionViewFlowLayout实例flowLayout。 - 设置其滚动方向为垂直(默认为垂直,也可设置为
.horizontal)。 - 创建
UICollectionView实例时传入该flowLayout,完成绑定。
2.1.2 FlowLayout的默认行为分析
默认情况下, UICollectionViewFlowLayout 以线性方式排列 item,支持以下默认行为:
- 滚动方向 :垂直或水平。
- item 大小 :统一大小(默认为
CGSize(width: 50, height: 50))。 - 间距设置 :行间距(
minimumLineSpacing)和 item 间距(minimumInteritemSpacing)。 - 对齐方式 :默认为左对齐(垂直滚动时)或上对齐(水平滚动时)。
| 属性名 | 默认值 | 作用说明 |
|---|---|---|
itemSize | CGSize(width: 50, height: 50) | 每个 item 的大小 |
minimumLineSpacing | 10 | 行间距 |
minimumInteritemSpacing | 10 | item 间距 |
sectionInset | UIEdgeInsets.zero | section 的内边距 |
scrollDirection | .vertical | 滚动方向 |
通过修改这些属性,可以快速调整布局样式,例如:
flowLayout.itemSize = CGSize(width: 100, height: 100)
flowLayout.minimumLineSpacing = 20
flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
这些设置直接影响 collectionView 的视觉表现。
2.2 自定义FlowLayout类的创建步骤
虽然 UICollectionViewFlowLayout 提供了基础的布局能力,但在实际开发中,往往需要更复杂的布局逻辑。此时,可以通过继承 UICollectionViewFlowLayout 并重写相关方法来实现自定义布局。
2.2.1 继承UICollectionViewFlowLayout并重写方法
创建自定义布局类的基本步骤如下:
-
定义子类 :
swift class CustomFlowLayout: UICollectionViewFlowLayout { // 自定义逻辑 } -
重写
prepare()方法 :
prepare()是布局类中最关键的方法,用于计算每个 item 的布局属性。例如:
swift override func prepare() { super.prepare() guard let collectionView = collectionView else { return } let itemCount = collectionView.numberOfItems(inSection: 0) for itemIndex in 0..<itemCount { let indexPath = IndexPath(item: itemIndex, section: 0) // 创建布局属性对象 let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) // 设置 frame attributes.frame = CGRect(x: CGFloat(itemIndex) * 110, y: 0, width: 100, height: 100) self.layoutAttributesCache.append(attributes) } }
逻辑说明:
- 在 prepare() 中计算每个 item 的位置和大小。
- 创建 UICollectionViewLayoutAttributes 对象并设置其 frame 。
- 将属性对象缓存起来供后续使用。
- 重写
layoutAttributesForElements(in:)方法 :
用于返回在指定 rect 范围内所有 item 的布局属性:
swift override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributesCache.filter { $0.frame.intersects(rect) } }
- 重写
layoutAttributesForItem(at:)方法 :
返回指定 indexPath 的 item 的布局属性:
swift override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributesCache[indexPath.item] }
2.2.2 设置布局属性与滚动方向
在自定义布局中,除了 item 的位置外,还可以控制滚动方向、边距等属性:
let customLayout = CustomFlowLayout()
customLayout.scrollDirection = .horizontal // 设置为水平滚动
customLayout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
通过这些设置,可以实现自定义的水平或垂直布局,并结合动画、手势等交互方式实现更丰富的 UI 效果。
2.3 布局参数的配置与调试技巧
在开发过程中,合理配置布局参数并进行调试是确保 UICollectionView 显示效果符合预期的关键。
2.3.1 itemSize、minimumLineSpacing等属性的作用
以下是一些常用布局属性及其作用:
| 属性名 | 作用说明 |
|---|---|
itemSize | 控制每个 item 的大小 |
minimumLineSpacing | 控制行与行之间的最小间距(垂直布局)或列与列之间的最小间距(水平布局) |
minimumInteritemSpacing | 控制同一行/列中 item 之间的最小间距 |
sectionInset | 控制 section 的边距 |
estimatedItemSize | 用于动态大小的 item(自动布局) |
示例代码:
flowLayout.itemSize = CGSize(width: 80, height: 80)
flowLayout.minimumLineSpacing = 15
flowLayout.minimumInteritemSpacing = 10
flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
2.3.2 使用模拟器调试布局效果
调试 UICollectionView 布局时,推荐使用以下方法:
- 使用模拟器查看布局边界 :
在viewDidLoad中添加如下代码,可显示每个 item 的边界:
swift collectionView.layer.borderWidth = 1.0 collectionView.layer.borderColor = UIColor.red.cgColor
- 打印 layout 属性 :
在prepare()方法中打印每个 item 的 frame:
swift print("Item \(indexPath.item) frame: $attributes.frame)")
-
使用 Xcode 的 Debug View Hierarchy 工具 :
在模拟器运行时点击调试按钮,选择 “Debug View Hierarchy”,可以查看所有视图的层级和 frame 信息。 -
动态调整布局参数 :
使用@IBInspectable在 Interface Builder 中动态修改属性,方便实时预览:
swift @IBInspectable var itemWidth: CGFloat = 100 { didSet { self.itemSize = CGSize(width: itemWidth, height: itemHeight) } }
通过本章的学习,我们了解了 UICollectionViewFlowLayout 的基本结构、如何创建自定义布局类以及如何配置和调试布局参数。这些知识为后续实现更复杂的瀑布流布局打下了坚实的基础。下一章将深入探讨如何实现瀑布流布局的核心逻辑。
3. 瀑布流布局的核心实现机制
瀑布流布局(Waterfall Layout)是 UICollectionView 中极具表现力的布局方式之一,广泛应用于图片墙、电商商品展示、社交媒体内容流等场景。与传统的 UICollectionViewFlowLayout 不同,瀑布流通过动态计算每个 Cell 的位置与大小,实现列数不固定、高度可变的复杂布局结构。本章将从布局核心方法入手,深入探讨瀑布流布局的实现机制,包括布局属性的定制、动态计算策略以及布局刷新的性能优化。
3.1 layoutAttributesForItemAtIndexPath方法的作用与重写逻辑
layoutAttributesForItemAtIndexPath: 是 UICollectionViewLayout 的一个关键方法,用于返回指定 indexPath 的 cell 应该显示的布局属性对象(UICollectionViewLayoutAttributes)。在瀑布流中,该方法需要被重写,以动态计算每个 cell 的 frame(位置与大小)。
3.1.1 布局属性对象的结构与作用
UICollectionViewLayoutAttributes 是描述 cell 布局信息的核心类,它包含了以下关键属性:
| 属性名 | 说明 |
|---|---|
| frame | cell 的位置和尺寸(CGRect) |
| center | cell 的中心点位置(CGPoint) |
| size | cell 的大小(CGSize) |
| transform | cell 的变换矩阵(如缩放、旋转) |
| alpha | cell 的透明度 |
| zIndex | cell 的层级关系(用于 Z 轴排序) |
在瀑布流中,我们需要为每个 cell 动态设置 frame 属性,以实现不规则排列。
3.1.2 计算每个Cell的布局位置与大小
在瀑布流中,通常使用一个数组来记录每列当前的 Y 值(即该列当前最后一个 cell 的底部位置),然后依次将每个 cell 放置在最短的那一列下方。
以下是一个典型的实现方式:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// 获取当前 item 的高度
CGFloat itemHeight = [self itemHeightForIndexPath:indexPath];
// 确定当前 item 应该插入哪一列
NSInteger shortestColumnIndex = [self findShortestColumn];
// 计算 item 的 frame
CGFloat x = shortestColumnIndex * (self.itemWidth + self.columnSpacing);
CGFloat y = self.columnHeights[shortestColumnIndex] + self.rowSpacing;
attributes.frame = CGRectMake(x, y, self.itemWidth, itemHeight);
// 更新该列的高度
self.columnHeights[shortestColumnIndex] = CGRectGetMaxY(attributes.frame);
return attributes;
}
代码逐行分析
- 第 1 行 :创建一个 UICollectionViewLayoutAttributes 对象,表示该 cell 的布局信息。
- 第 4 行 :调用自定义方法
itemHeightForIndexPath:动态获取该 cell 的高度(通常基于图片宽高比)。 - 第 7 行 :调用
findShortestColumn方法找到当前列中最短的一列,确保 cell 放置在最紧凑的位置。 - 第 10~11 行 :根据列宽和间距计算 cell 的 x 坐标,y 坐标则基于该列当前高度加上行间距。
- 第 14 行 :设置 cell 的 frame。
- 第 17 行 :更新该列的当前高度,为下一个 cell 的排列做准备。
Mermaid 流程图:瀑布流布局属性计算流程
graph TD
A[开始] --> B{indexPath有效?}
B -->|是| C[创建layoutAttributes对象]
C --> D[获取item高度]
D --> E[找到最短列]
E --> F[计算x,y坐标]
F --> G[设置frame]
G --> H[更新列高度]
H --> I[返回attributes]
B -->|否| J[返回nil]
3.2 sizeForItemAtIndexPath代理方法的实现细节
UICollectionViewDelegateFlowLayout 提供了 collectionView:layout:sizeForItemAtIndexPath: 方法,用于动态计算每个 cell 的大小。在瀑布流中,该方法通常用于根据图片内容动态调整 cell 高度,以保持视觉上的统一和美观。
3.2.1 动态计算图片尺寸的策略
在实际开发中,图片的宽高比可能不一致,若使用固定高度会造成布局不协调。因此,我们通常采用以下策略:
- 固定 cell 的宽度(如屏幕宽度 / 2 - 间距)。
- 根据图片原始宽高比,计算出对应的高度。
- 返回
CGSizeMake(width, height)作为 cell 的尺寸。
示例代码如下:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// 假设图片数据为 NSDictionary,包含原始尺寸
NSDictionary *imageInfo = self.imageData[indexPath.item];
CGFloat originalWidth = [imageInfo[@"width"] floatValue];
CGFloat originalHeight = [imageInfo[@"height"] floatValue];
// 固定 cell 宽度
CGFloat cellWidth = (collectionView.bounds.size.width - self.columnSpacing * (self.numberOfColumns - 1)) / self.numberOfColumns;
// 按比例计算高度
CGFloat cellHeight = cellWidth * (originalHeight / originalWidth);
return CGSizeMake(cellWidth, cellHeight);
}
参数说明与逻辑分析
- collectionView.bounds.size.width :获取当前 collectionView 的宽度。
- self.columnSpacing :列之间的间距。
- self.numberOfColumns :设定的列数(如 2 或 3)。
- cellWidth :根据列数和间距动态计算出每一列的可用宽度。
- originalWidth / originalHeight :图片的原始宽高比。
- cellHeight :基于 cellWidth 和宽高比计算出的高度。
3.2.2 结合图片宽高比进行自适应调整
在实际项目中,为了提升用户体验,通常还需要对图片进行裁剪、缩放处理,确保在不同设备和屏幕尺寸下都能良好显示。可以通过以下方式优化:
- 使用
SDWebImage或Kingfisher异步加载图片后回调宽高比; - 提前加载图片元数据获取尺寸;
- 对图片进行等比缩放,保持其宽高比不变。
3.3 布局刷新与invalidateLayout的使用场景
在 UICollectionView 中,当数据源或布局参数发生变化时,需要手动触发布局刷新,以确保 UI 显示正确。其中, invalidateLayout 是一个核心方法,用于通知系统当前布局已失效,需要重新计算。
3.3.1 invalidateLayout的触发条件
invalidateLayout 通常在以下情况下调用:
- 数据源发生变化(如新增、删除、排序);
- 布局参数被修改(如列数、间距、滚动方向);
- 图片尺寸发生改变(如用户旋转设备);
- 手动刷新布局(如点击“刷新”按钮)。
调用方式如下:
[self.collectionView.collectionViewLayout invalidateLayout];
3.3.2 刷新布局时的性能优化策略
频繁调用 invalidateLayout 会导致布局重新计算,影响性能。为此,可以采用以下优化策略:
| 优化策略 | 描述 |
|---|---|
| 缓存布局信息 | 将 layoutAttributes 缓存下来,避免重复计算 |
| 批量刷新 | 使用 performBatchUpdates:completion: 批量更新 |
| 防止重复刷新 | 添加刷新锁或标记,避免重复触发 |
| 懒加载布局属性 | 只在真正需要时才计算 layoutAttributes |
示例:缓存 layoutAttributes
@property (nonatomic, strong) NSMutableDictionary<NSIndexPath *, UICollectionViewLayoutAttributes *> *cachedAttributes;
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = self.cachedAttributes[indexPath];
if (!attributes) {
attributes = [self calculateAttributesForIndexPath:indexPath];
self.cachedAttributes[indexPath] = attributes;
}
return attributes;
}
逻辑分析
- 使用
NSMutableDictionary缓存每个 indexPath 对应的 layoutAttributes; - 每次调用
layoutAttributesForItemAtIndexPath:时先检查缓存是否存在; - 若不存在则重新计算并缓存,避免重复计算。
总结性说明
本章深入讲解了瀑布流布局的三大核心机制:布局属性的动态计算、cell 尺寸的自适应调整、以及布局刷新的触发与优化。通过对 layoutAttributesForItemAtIndexPath: 和 sizeForItemAtIndexPath: 的重写,我们实现了灵活的瀑布流布局;而 invalidateLayout 的合理使用则确保了布局在数据变化时能够及时刷新,同时通过缓存等策略提升性能。下一章将围绕图片异步加载展开,探讨如何进一步优化瀑布流的整体性能与用户体验。
4. 图片异步加载与性能优化策略
在iOS开发中,尤其是涉及大量图片展示的场景下,如社交应用、电商首页、瀑布流等,图片的加载效率直接影响用户体验和应用的性能表现。本章将围绕图片异步加载机制、图片尺寸预加载计算、懒加载与预加载策略等核心内容展开,重点探讨如何在UICollectionView中实现高效的图片加载与渲染优化。
4.1 图片异步加载的基本原理
图片异步加载是iOS应用中处理网络图片展示的核心技术之一。其核心目标是避免主线程阻塞,从而防止界面卡顿、响应延迟等问题。
4.1.1 SDWebImage与Kingfisher的工作机制
SDWebImage 和 Kingfisher 是iOS开发中最常用的两个图片加载库,它们都基于异步加载和缓存策略来提升图片展示性能。
- SDWebImage 基于 Objective-C 编写,广泛用于 UIKit 项目中。
- Kingfisher 是 Swift 编写的库,专为 Swift 和 SwiftUI 项目设计。
两者的异步加载流程基本一致,如下图所示:
graph TD
A[发起图片加载请求] --> B{本地缓存是否存在}
B -- 是 --> C[直接加载本地缓存]
B -- 否 --> D[检查内存缓存]
D -- 是 --> E[从内存缓存加载]
D -- 否 --> F[发起网络请求下载图片]
F --> G[下载完成后缓存到内存和磁盘]
G --> H[回调主线程更新UI]
SDWebImage 示例代码:
import SDWebImage
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let url = URL(string: "https://example.com/image.jpg")
imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder"))
代码解析:
-
sd_setImage(with:placeholderImage:)是 SDWebImage 提供的便捷方法,用于异步加载并设置图片。 -
with参数指定图片的远程 URL。 -
placeholderImage是在图片加载完成前显示的占位图。
Kingfisher 示例代码:
import Kingfisher
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let url = URL(string: "https://example.com/image.jpg")
imageView.kf.setImage(with: url, placeholder: UIImage(named: "placeholder"))
代码解析:
-
kf.setImage(with:placeholder:)是 Kingfisher 的核心方法。 - 支持更高级的配置,如过渡动画、缓存策略等。
4.1.2 图片缓存与下载策略的对比
| 特性 | SDWebImage | Kingfisher |
|---|---|---|
| 开发语言 | Objective-C | Swift |
| 支持平台 | iOS、tvOS、macOS | iOS、macOS、watchOS、Linux |
| 缓存机制 | 支持内存+磁盘缓存 | 支持多级缓存策略 |
| 网络请求 | 基于 NSURLSession | 基于 URLSession |
| 可配置性 | 中等 | 高 |
| 动画支持 | 有限 | 支持过渡动画、缩放等 |
Kingfisher 在 Swift 项目中更具优势,尤其是在结合 SwiftUI 使用时;而 SDWebImage 更适合维护较老的 UIKit 项目。
4.2 图片尺寸预加载与计算优化
在瀑布流布局中,每个Cell的高度通常是根据图片的宽高比动态计算的。如果在图片加载完成后才进行高度计算,会导致布局抖动或界面重绘,影响用户体验。因此, 提前获取图片的元数据(如尺寸)并进行预计算 非常关键。
4.2.1 异步获取图片元数据
我们可以借助 URLSession 异步下载图片数据,并仅解析其元数据,而不加载完整图片内容。
func fetchImageMetadata(from url: URL, completion: @escaping (CGSize?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
completion(nil)
return
}
let source = CGImageSourceCreateWithData(data as CFData, nil)
guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source!, 0, nil) as? [CFString: Any],
let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int,
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int else {
completion(nil)
return
}
let size = CGSize(width: pixelWidth, height: pixelHeight)
completion(size)
}
task.resume()
}
代码逻辑分析:
- 使用
URLSession异步下载图片数据。 - 利用
CGImageSource解析图片头信息,获取其宽高。 - 在不加载整张图片的前提下完成尺寸获取。
4.2.2 提前计算Cell高度以避免布局抖动
在 UICollectionView 的瀑布流布局中,我们通常会实现 collectionView(_:layout:sizeForItemAt:) 方法来动态计算每个 Cell 的大小。
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let originalWidth = imageWidths[indexPath.item]
let cellWidth = (UIScreen.main.bounds.width - 30) / 2 // 假设双列布局
let ratio = CGFloat(originalWidth.height) / CGFloat(originalWidth.width)
return CGSize(width: cellWidth, height: cellWidth * ratio)
}
参数说明:
-
imageWidths是一个数组,保存了每张图片的原始宽高信息。 -
cellWidth是 Cell 的固定宽度(根据屏幕宽度和列数计算)。 -
ratio是图片的高宽比,用于计算高度。
通过提前获取图片尺寸并在布局中使用,可以避免 Cell 加载图片后重新计算布局,从而避免“跳动”现象。
4.3 瀑布流中的懒加载与预加载技术
在瀑布流布局中,一次性加载所有图片会导致内存占用过高,甚至引发性能问题。为此,我们引入“懒加载”与“预加载”机制,实现按需加载图片。
4.3.1 可见区域外图片的延迟加载
UICollectionView 提供了 indexPathsForVisibleItems 方法,可以获取当前可见的 Cell。
我们可以在 cellForItemAt 中判断当前 Cell 是否在可视区域内,如果不在,则不立即加载图片。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCollectionViewCell
if collectionView.indexPathsForVisibleItems.contains(indexPath) {
let url = imageURLs[indexPath.item]
cell.imageView.kf.setImage(with: url)
} else {
cell.imageView.image = nil
}
return cell
}
逻辑说明:
- 若 Cell 可见,则加载图片。
- 否则,清空图片以减少内存占用。
此外,我们还可以监听滚动事件,在滚动停止后加载图片:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
loadImagesForVisibleCells()
}
func loadImagesForVisibleCells() {
guard let visibleIndexPaths = collectionView.indexPathsForVisibleItems as? [IndexPath] else { return }
for indexPath in visibleIndexPaths {
let cell = collectionView.cellForItem(at: indexPath) as! CustomCollectionViewCell
let url = imageURLs[indexPath.item]
cell.imageView.kf.setImage(with: url)
}
}
4.3.2 预加载策略的设计与实现
预加载指的是在用户即将滚动到某个区域时,提前加载那部分数据。我们可以使用 UICollectionViewDataSourcePrefetching 协议实现预加载。
extension ViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let url = imageURLs[indexPath.item]
KingfisherManager.shared.retrieveImage(with: url, options: nil, progressBlock: nil) { result in
switch result {
case .success(let value):
print("Prefetched image: $value.image)")
case .failure(let error):
print("Failed to prefetch image: $error.localizedDescription)")
}
}
}
}
}
实现逻辑:
- 当 UICollectionView 即将滚动到某个 Cell 时,系统会调用
prefetchItemsAt。 - 我们在此处提前加载图片数据并缓存。
- 当用户真正滚动到该 Cell 时,图片已缓存,可快速展示。
结合懒加载与预加载策略,可以显著提升瀑布流的加载速度与用户体验。
小结与下章预告
本章详细讲解了在 UICollectionView 中实现图片异步加载的关键机制,包括主流库 SDWebImage 与 Kingfisher 的使用、图片元数据的异步获取、Cell 尺寸的预计算方法,以及瀑布流中的懒加载与预加载技术。这些技术组合使用,可以有效避免界面卡顿、内存占用过高、布局抖动等问题。
下一章将进入 UICollectionView 的数据源与交互处理部分,深入讲解如何实现高效的数据绑定、多Section支持、以及用户交互事件的响应机制。
5. UICollectionView的数据源与交互处理
在构建 iOS 应用的过程中,UICollectionView 作为展示复杂数据集合的首选控件,其背后的数据源和交互逻辑是支撑整个视图动态展示与用户操作的核心机制。本章将围绕 UICollectionView 的数据源(DataSource)与交互代理(Delegate)进行深入剖析,从数据绑定到用户行为响应,再到多 Section 的支持与事件触发机制,帮助开发者全面掌握如何高效地构建和管理集合视图的数据流与交互体验。
5.1 UICollectionViewDataSource的实现
UICollectionViewDataSource 是 UICollectionView 的核心数据提供者,它负责向 UICollectionView 提供所需的数据和结构信息。理解并正确实现该协议,是保证集合视图正常显示和高效运行的基础。
5.1.1 必要方法的实现与数据绑定
UICollectionViewDataSource 协议中必须实现的两个方法是:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
这两个方法分别用于提供每个 section 中 item 的数量以及每个 item 所对应的 UICollectionViewCell。
示例代码:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.dataSource[section].items.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
ItemModel *item = self.dataSource[indexPath.section].items[indexPath.row];
cell.titleLabel.text = item.title;
cell.imageView.image = item.thumbnail;
return cell;
}
逻辑分析:
-
numberOfItemsInSection:方法返回当前 section 的 item 总数。该方法决定了 UICollectionView 在某个 section 中需要展示多少个单元格。 -
cellForItemAtIndexPath:方法负责为特定索引路径(NSIndexPath)创建或重用一个 UICollectionViewCell。使用dequeueReusableCellWithReuseIdentifier:forIndexPath:可以高效地从缓存池中取出一个已存在的 cell,避免重复创建造成性能损耗。 -
ItemModel是开发者自定义的数据模型类,用于封装每个 item 的内容,如标题、图片等信息。
参数说明:
-
collectionView: 当前的 UICollectionView 实例。 -
indexPath: 一个 NSIndexPath 对象,表示当前 item 所在的 section 和 row。 -
dequeueReusableCellWithReuseIdentifier:forIndexPath:: 通过标识符从缓存池中取出可重用的 cell,如果没有则会自动创建。
5.1.2 多Section支持与数据组织方式
UICollectionView 支持多个 section 的展示,这对于实现复杂的布局结构(如分类展示、分组数据)非常关键。实现多 section 支持需要实现如下方法:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
示例代码:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return self.dataSource.count;
}
此外,为了实现每个 section 的标题,还可以实现 collectionView:viewForSupplementaryElementOfKind:atIndexPath: 方法来提供 header 或 footer 的视图。
数据组织方式建议:
通常使用数组嵌套结构来管理数据源,例如:
NSArray<SectionModel *> *dataSource; // SectionModel 包含 items 数组
这样每个 SectionModel 对象包含一个 items 数组,便于管理 section 内的数据项,同时也可以为每个 section 添加标题、描述等元信息。
5.2 UICollectionViewDelegate的交互处理
UICollectionViewDelegate 协议负责处理 UICollectionView 的用户交互事件,包括点击、高亮、滚动等行为。合理使用该协议,可以增强用户与集合视图之间的互动体验。
5.2.1 Cell点击与高亮事件响应
UICollectionViewDelegate 提供了以下方法来处理 cell 的点击和高亮状态:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath;
示例代码:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
ItemModel *item = self.dataSource[indexPath.section].items[indexPath.row];
NSLog(@"Selected item: %@", item.title);
// 跳转到详情页或执行其他操作
}
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = [UIColor lightGrayColor];
}
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = [UIColor whiteColor];
}
逻辑分析:
-
didSelectItemAtIndexPath:方法在用户点击某个 cell 时触发,通常用于跳转页面或展示详细信息。 -
didHighlightItemAtIndexPath:和didUnhighlightItemAtIndexPath:分别在 cell 被按下和释放时调用,可用于实现高亮反馈效果,如改变背景色或缩放动画。 - 使用
cellForItemAtIndexPath:获取当前 cell,进而操作其子视图(如背景色、图片、标签等)。
参数说明:
-
indexPath: 表示被点击或高亮的 cell 的索引路径。 -
cell.contentView: cell 的内容视图,是自定义 UI 的主要容器。
5.2.2 滚动事件与加载更多数据的触发
UICollectionView 的滚动事件可以通过 UIScrollViewDelegate 的方法来监听,因为 UICollectionView 继承自 UIScrollView。常见的使用场景是当用户滚动到列表底部时,自动加载更多数据。
示例代码:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height;
CGFloat scrollViewHeight = scrollView.frame.size.height;
if (offsetY > contentHeight - scrollViewHeight * 1.5) {
[self loadMoreData];
}
}
- (void)loadMoreData {
if (self.isLoading) return;
self.isLoading = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟网络请求
sleep(1);
dispatch_async(dispatch_get_main_queue(), ^{
// 更新数据源
[self.dataSource addObjectsFromArray:[self fetchMoreItems]];
[self.collectionView reloadData];
self.isLoading = NO;
});
});
}
逻辑分析:
-
scrollViewDidScroll:是 UIScrollViewDelegate 的方法,每当用户滚动时都会调用。 - 判断是否接近底部(
offsetY > contentHeight - scrollViewHeight * 1.5),如果是,则触发loadMoreData方法。 -
loadMoreData方法中模拟了异步加载数据的过程,使用 GCD 进行线程切换,确保 UI 不被阻塞。 - 加载完成后更新数据源并刷新 UICollectionView。
参数说明:
-
contentOffset.y: 当前滚动的偏移量。 -
contentSize.height: 整个内容区域的高度。 -
frame.size.height: 可视区域的高度。
表格:UICollectionViewDataSource 与 UICollectionViewDelegate 的关键方法对比
| 协议类型 | 方法名 | 功能描述 |
|---|---|---|
| UICollectionViewDataSource | numberOfItemsInSection: | 返回每个 section 中的 item 数量 |
| UICollectionViewDataSource | cellForItemAtIndexPath: | 提供每个 item 的 cell |
| UICollectionViewDataSource | numberOfSectionsInCollectionView: | 返回 section 的总数 |
| UICollectionViewDelegate | didSelectItemAtIndexPath: | 用户点击 cell 时触发 |
| UICollectionViewDelegate | didHighlightItemAtIndexPath: | cell 被高亮时触发 |
| UICollectionViewDelegate | scrollViewDidScroll: | 用户滚动 UICollectionView 时触发 |
Mermaid 流程图:UICollectionView 数据绑定与交互流程
graph TD
A[UICollectionView 初始化] --> B[设置 DataSource 和 Delegate]
B --> C[调用 numberOfSectionsInCollectionView:]
C --> D[调用 numberOfItemsInSection:]
D --> E[调用 cellForItemAtIndexPath:]
E --> F[渲染 UICollectionViewCell]
F --> G{用户交互事件?}
G -->|点击| H[调用 didSelectItemAtIndexPath:]
G -->|高亮| I[调用 didHighlightItemAtIndexPath:]
G -->|滚动| J[调用 scrollViewDidScroll:]
H --> K[处理点击逻辑]
I --> L[更新高亮样式]
J --> M[判断是否加载更多]
M -->|是| N[调用 loadMoreData]
M -->|否| O[继续滚动]
本章内容详细解析了 UICollectionView 的数据源实现机制与交互处理逻辑,包括数据绑定、多 section 支持、点击高亮响应以及滚动加载更多数据的实现方式。通过掌握这些关键知识点,开发者可以构建出更加灵活、响应迅速的集合视图应用。
6. UICollectionView性能优化与Cell重用机制
在iOS开发中,UICollectionView作为展示复杂列表数据的核心组件,其性能优化与Cell重用机制是提升用户体验的关键。尤其是在实现瀑布流布局时,大量Cell的动态创建、布局计算和图片加载,容易造成内存压力和界面卡顿。本章将深入解析UICollectionView的Cell重用机制,探讨瀑布流场景下的性能瓶颈,并提出多种优化策略,以实现流畅、高效的交互体验。
6.1 Cell重用机制详解
UICollectionView通过高效的Cell重用机制来降低内存消耗并提升滚动性能。其核心在于 dequeueReusableCellWithReuseIdentifier: 方法的实现原理,以及对自定义Cell生命周期的合理管理。
6.1.1 dequeueReusableCellWithReuseIdentifier的原理
当UICollectionView需要展示一个Cell时,它并不会每次都创建新的实例,而是优先从 重用队列(reuse queue) 中取出一个可重用的Cell对象。这种机制可以显著减少内存分配和初始化的开销。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
// 配置cell内容
return cell;
}
逐行分析:
- 第2行:调用
dequeueReusableCellWithReuseIdentifier:forIndexPath:方法,尝试从重用池中获取一个可用的Cell。 - 第3行:配置Cell内容,如设置图片、文本等。
该方法的底层实现依赖于 UICollectionView 内部维护的多个重用池(每个 reuseIdentifier 对应一个池)。当Cell滑出屏幕时,系统将其放入对应的池中,等待下一次复用。
重用机制的优势:
| 优势 | 描述 |
|---|---|
| 内存节省 | 避免频繁创建和销毁Cell对象 |
| 性能提升 | 减少UI渲染的延迟,提升滚动流畅度 |
| 易于管理 | 统一管理不同类型的Cell实例 |
6.1.2 自定义Cell的生命周期管理
自定义UICollectionViewCell时,开发者需要合理管理其生命周期,以确保在重用过程中不会出现数据错乱或资源泄露的问题。
@interface MyCustomCell : UICollectionViewCell
@property (nonatomic, strong) UIImageView *imageView;
@end
@implementation MyCustomCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_imageView = [[UIImageView alloc] initWithFrame:self.contentView.bounds];
_imageView.contentMode = UIViewContentModeScaleAspectFill;
_imageView.clipsToBounds = YES;
[self.contentView addSubview:_imageView];
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
// 清理内容,防止重用污染
self.imageView.image = nil;
}
@end
逐行分析:
- 第1~5行:定义自定义Cell类,包含一个UIImageView属性。
- 第7~13行:在
initWithFrame:方法中初始化UI元素。 - 第15~19行:重写
prepareForReuse方法,在Cell即将被重用时清空内容。
生命周期关键方法说明:
| 方法 | 调用时机 | 用途 |
|---|---|---|
initWithFrame: | Cell首次创建时 | 初始化UI控件 |
prepareForReuse | Cell将被重用时 | 清理旧数据、释放资源 |
willMoveToSuperview: | Cell即将添加到父视图时 | 做布局前的准备 |
didMoveToSuperview | Cell已添加到父视图时 | 做布局后的处理 |
合理管理Cell生命周期,有助于避免内存泄漏、图像错位等问题,特别是在异步加载图片时尤为重要。
6.2 瀑布流中的性能瓶颈与优化手段
瀑布流布局由于每个Cell的高度不一致,导致布局计算复杂度较高,容易成为性能瓶颈。本节将分析常见性能问题,并提供优化策略。
6.2.1 布局计算与渲染效率的提升
在瀑布流中,每个Cell的位置需要根据当前列的最低高度进行计算,频繁调用 layoutAttributesForElementsInRect: 和 layoutAttributesForItemAtIndexPath: 会导致性能下降。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat width = self.collectionView.bounds.size.width;
CGFloat columnCount = 2;
CGFloat itemWidth = (width - (columnCount - 1) * self.minimumColumnSpacing) / columnCount;
// 计算列索引
NSInteger column = indexPath.item % (NSInteger)columnCount;
// 计算x坐标
CGFloat x = (itemWidth + self.minimumColumnSpacing) * column;
// 计算y坐标
CGFloat y = self.heightForColumn[column];
attributes.frame = CGRectMake(x, y, itemWidth, self.heightForItems[indexPath.item]);
self.heightForColumn[column] = CGRectGetMaxY(attributes.frame) + self.minimumLineSpacing;
return attributes;
}
逐行分析:
- 第1行:重写
layoutAttributesForItemAtIndexPath:方法。 - 第3~5行:计算Cell的宽度,考虑列间距。
- 第8~10行:根据列索引计算Cell的坐标。
- 第12~13行:更新列的最低高度,为下一个Cell提供位置依据。
优化建议:
- 预计算布局属性 :在
prepareLayout中预先计算所有Cell的位置,并缓存起来,避免每次调用都重新计算。 - 减少冗余调用 :通过
invalidateLayout控制刷新时机,避免频繁触发布局计算。 - 使用懒加载 :在首次布局时,仅计算可视区域的Cell,其余位置延迟加载。
graph TD
A[开始布局计算] --> B{是否首次布局}
B -->|是| C[预计算所有Cell位置]
B -->|否| D[仅更新变化区域]
C --> E[缓存布局属性]
D --> F[重用已有属性]
E --> G[返回布局属性]
F --> G
6.2.2 内存占用与图片加载的优化策略
瀑布流中通常需要加载大量图片,若不加以优化,易导致内存占用过高,甚至OOM(Out Of Memory)崩溃。
常见问题:
- 多张高清图片同时加载,占用大量内存。
- 图片未及时释放,导致缓存膨胀。
- Cell重用时未取消之前的加载请求,出现图片错位。
优化方案:
- 使用高效图片加载库 :如SDWebImage或Kingfisher,支持异步加载、缓存和取消机制。
- 图片尺寸适配 :加载图片时根据Cell实际显示尺寸进行缩放,避免加载高清大图。
- 内存缓存限制 :使用
NSCache并设置最大内存占用上限,避免内存溢出。
- (void)configureCell:(MyCustomCell *)cell forIndexPath:(NSIndexPath *)indexPath {
NSString *imageUrl = self.imageUrls[indexPath.item];
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:imageUrl]
placeholderImage:[UIImage imageNamed:@"placeholder"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
if (image) {
// 图片加载完成后刷新布局
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadItemsAtIndexPaths:@[indexPath]];
} completion:nil];
}
}];
}
逐行分析:
- 第2行:使用SDWebImage异步加载图片。
- 第5~9行:图片加载完成后刷新Cell,确保布局正确。
优化对比:
| 未优化 | 优化后 |
|---|---|
| 每次加载高清图,内存占用高 | 按需加载适配尺寸,内存更可控 |
| Cell重用时图片错位 | 使用SDWebImage自动取消请求 |
| 无缓存策略,加载速度慢 | 使用内存+磁盘缓存,提升加载速度 |
6.3 高效内存管理与缓存策略
良好的内存管理是保障应用稳定运行的关键,尤其在瀑布流中涉及大量图片加载与Cell重用。
6.3.1 NSCache与自定义缓存机制
NSCache 是一个线程安全的内存缓存类,适合用于缓存Cell的布局信息或图片数据。
@property (nonatomic, strong) NSCache<NSString *, id> *layoutCache;
- (void)setupCache {
self.layoutCache = [[NSCache alloc] init];
self.layoutCache.countLimit = 100; // 最多缓存100个布局信息
self.layoutCache.delegate = self;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
NSString *key = [NSString stringWithFormat:@"cell_%d", indexPath.item];
UICollectionViewLayoutAttributes *attributes = [self.layoutCache objectForKey:key];
if (!attributes) {
attributes = [self computeLayoutForIndexPath:indexPath];
[self.layoutCache setObject:attributes forKey:key];
}
return attributes;
}
逐行分析:
- 第1行:声明NSCache属性。
- 第3~6行:初始化NSCache并设置缓存策略。
- 第8~16行:从缓存中获取或计算布局属性。
NSCache优势:
| 特性 | 说明 |
|---|---|
| 自动释放 | 当内存紧张时自动释放部分缓存 |
| 线程安全 | 可在多线程中安全使用 |
| 快速访问 | 支持O(1)级别的读取效率 |
6.3.2 Cell内容的重用与清理逻辑
在Cell滑出屏幕后,应及时清理其内容,避免内存泄漏。
- (void)prepareForReuse {
[super prepareForReuse];
self.imageView.image = nil;
self.titleLabel.text = nil;
[self.imageView cancelCurrentImageLoad]; // 若使用Kingfisher等库
}
逐行分析:
- 第3行:清空图片内容。
- 第4行:清空文本内容。
- 第5行:取消正在进行的图片加载请求,防止内存泄漏。
清理策略建议:
| 内容类型 | 清理方式 |
|---|---|
| 图片 | 设置为nil,取消加载请求 |
| 网络请求 | 使用CancelToken或delegate取消 |
| 动画 | 移除所有动画并置空相关对象 |
| 数据模型 | 释放强引用,避免循环引用 |
通过深入理解UICollectionView的Cell重用机制、布局计算优化策略以及内存管理技巧,开发者可以在实现复杂瀑布流布局时兼顾性能与用户体验。下一章将继续探讨如何将这些功能封装成可复用模块,提升代码的可维护性与可扩展性。
7. 瀑布流布局的代码封装与模块化设计
7.1 自定义FlowLayout的封装策略
在实际开发中,为了提高代码的复用性与可维护性,我们需要将瀑布流布局的核心逻辑进行封装。一个良好的封装策略可以使得同一个布局逻辑适用于多个项目,并且易于扩展。
7.1.1 提取通用布局逻辑为独立类
我们可以创建一个继承自 UICollectionViewFlowLayout 的自定义类 WaterfallFlowLayout ,并在其中实现瀑布流的核心计算逻辑。
import UIKit
class WaterfallFlowLayout: UICollectionViewFlowLayout {
var numberOfColumns: Int = 2
private var attributesCache = [UICollectionViewLayoutAttributes]()
private var contentHeight: CGFloat = 0.0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else { return 0 }
return collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var columnHeights = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let column = findShortestColumnIndex(columnHeights)
let itemWidth = columnWidth
let itemHeight = calculateItemHeight(itemWidth: itemWidth, at: indexPath)
let frame = CGRect(x: CGFloat(column) * columnWidth, y: columnHeights[column], width: itemWidth, height: itemHeight)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
attributesCache.append(attributes)
columnHeights[column] += itemHeight
}
contentHeight = columnHeights.max() ?? 0
}
private func findShortestColumnIndex(_ heights: [CGFloat]) -> Int {
return heights.firstIndex(of: heights.min()!) ?? 0
}
private func calculateItemHeight(itemWidth: CGFloat, at indexPath: IndexPath) -> CGFloat {
// 此处应由外部传入图片宽高比或自定义计算方式
let imageWidth: CGFloat = 100
let imageHeight: CGFloat = 150
return itemWidth * imageHeight / imageWidth
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return attributesCache[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesCache.filter { rect.intersects($0.frame) }
}
}
7.1.2 支持不同方向与列数配置
通过在 WaterfallFlowLayout 中添加方向控制和列数配置属性,可以支持不同场景下的布局需求。
var scrollDirection: UICollectionView.ScrollDirection = .vertical {
didSet {
self.collectionView?.collectionViewLayout.invalidateLayout()
}
}
在 prepare() 方法中根据 scrollDirection 设置 UICollectionViewLayout 的滚动方向。
7.2 数据加载与布局的分离设计
为了实现更清晰的职责划分,我们应将数据加载与布局计算逻辑进行分离。这种设计可以提高代码的可测试性与可维护性。
7.2.1 ViewModel与布局逻辑解耦
使用 ViewModel 来管理数据加载与处理逻辑,将图片 URL、宽高比等信息传递给布局类。
struct WaterfallItem {
let imageURL: URL
let aspectRatio: CGFloat
}
class WaterfallViewModel {
var items = [WaterfallItem]()
func loadItems(completion: @escaping () -> Void) {
// 模拟异步加载数据
DispatchQueue.global().async {
// 从网络获取数据
self.items = [
WaterfallItem(imageURL: URL(string: "https://example.com/image1.jpg")!, aspectRatio: 0.75),
WaterfallItem(imageURL: URL(string: "https://example.com/image2.jpg")!, aspectRatio: 1.33)
]
DispatchQueue.main.async {
completion()
}
}
}
}
在 WaterfallFlowLayout 中添加对 aspectRatio 的支持:
private var itemAspectRatios = [IndexPath: CGFloat]()
func setAspectRatio(_ ratio: CGFloat, forItemAt indexPath: IndexPath) {
itemAspectRatios[indexPath] = ratio
}
private func calculateItemHeight(itemWidth: CGFloat, at indexPath: IndexPath) -> CGFloat {
guard let ratio = itemAspectRatios[indexPath] else { return itemWidth }
return itemWidth * ratio
}
7.2.2 使用协议与回调实现灵活交互
通过定义协议,可以让布局类与 ViewModel 之间进行解耦通信。
protocol WaterfallLayoutDelegate: AnyObject {
func waterfallLayout(_ layout: WaterfallFlowLayout, aspectRatioForItemAt indexPath: IndexPath) -> CGFloat
}
class WaterfallFlowLayout: UICollectionViewFlowLayout {
weak var delegate: WaterfallLayoutDelegate?
private func calculateItemHeight(itemWidth: CGFloat, at indexPath: IndexPath) -> CGFloat {
guard let delegate = delegate else { return itemWidth }
return itemWidth * delegate.waterfallLayout(self, aspectRatioForItemAt: indexPath)
}
}
7.3 瀑布流组件的模块化与复用
为了在多个项目中高效复用瀑布流布局组件,我们需要将其封装为一个模块化的独立组件。
7.3.1 组件化设计思想在UICollectionView中的应用
组件化设计的核心是将功能封装为独立模块,提供清晰的接口供外部调用。我们可以将 WaterfallFlowLayout 、 WaterfallViewModel 、 WaterfallCell 等类组织为一个独立的 Swift Package 或 Framework。
目录结构示例:
WaterfallComponent/
├── Layout/
│ └── WaterfallFlowLayout.swift
├── ViewModel/
│ └── WaterfallViewModel.swift
├── Cell/
│ └── WaterfallCollectionViewCell.swift
└── Protocol/
└── WaterfallLayoutDelegate.swift
7.3.2 封装成独立组件供多个项目使用
通过将上述组件封装为 Swift Package,可以在多个项目中快速集成。
步骤如下:
- 创建 Swift Package:
swift package init --type library
-
将上述文件放入
Sources/WaterfallComponent/目录中。 -
在
Package.swift中定义产品:
targets: [
.target(
name: "WaterfallComponent",
dependencies: []),
.testTarget(
name: "WaterfallComponentTests",
dependencies: ["WaterfallComponent"]),
]
-
在项目中通过 Swift Package Manager 引入该组件。
-
在 ViewController 中使用:
let layout = WaterfallFlowLayout()
layout.delegate = self
layout.numberOfColumns = 3
collectionView.collectionViewLayout = layout
viewModel.loadItems {
collectionView.reloadData()
}
(本章完)
简介:在iOS开发中,UICollectionView通过自定义布局可实现如Pinterest风格的瀑布流效果。本资源“ios-collectionView实现瀑布流.zip”提供了一套完整的瀑布流实现代码,涵盖UICollectionViewFlowLayout子类自定义、图片异步加载与尺寸计算、数据源配置、Cell重用机制、布局刷新及性能优化等关键技术点。开发者可快速集成并适配数据源,打造流畅美观的瀑布流界面。
2217

被折叠的 条评论
为什么被折叠?



