版本記錄
| 版本號 | 時(shí)間 |
|---|---|
| V1.0 | 2019.09.16 星期一 |
前言
iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動力學(xué)和移動效果(一)
2. UIKit框架(二) —— UIKit動力學(xué)和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (二)
開始
首先看下主要內(nèi)容
構(gòu)建一個(gè)受
UICollectionView自定義布局,并學(xué)習(xí)如何緩存屬性并動態(tài)調(diào)整單元格大小。
下面看下寫作環(huán)境
Swift 5, iOS 13, Xcode 11
iOS 6中引入的UICollectionView已成為iOS開發(fā)人員中最受歡迎的UI元素之一。它如此吸引人的是數(shù)據(jù)和表示層之間的分離,這取決于處理布局的單獨(dú)對象。然后,布局負(fù)責(zé)確定視圖的放置和視覺屬性。
您可能使用了默認(rèn)的流布局,即UIKit提供的布局類。這是一個(gè)帶有一些自定義的基本網(wǎng)格布局。
但您也可以實(shí)現(xiàn)自己的自定義布局,以任何方式排列視圖。這使得集合視圖具有靈活性和強(qiáng)大功能。
在這個(gè)UICollectionView自定義布局教程中,您將創(chuàng)建一個(gè)受流行的Pinterest應(yīng)用程序啟發(fā)的布局。
在此過程中,您將學(xué)習(xí):
- 關(guān)于自定義布局。
- 如何計(jì)算和緩存布局屬性。
- 如何處理動態(tài)大小的單元格。
在Xcode中打開下載好的項(xiàng)目并啟動項(xiàng)目。
構(gòu)建并運(yùn)行項(xiàng)目。 你會看到以下內(nèi)容:

該應(yīng)用程序提供了RWDevCon的照片庫。 您可以瀏覽照片,看看與會者在會議期間有多么有趣。
該庫使用具有標(biāo)準(zhǔn)流布局的集合視圖。 乍一看,它看起來還不錯(cuò)。 但你當(dāng)然可以改進(jìn)布局設(shè)計(jì)。
照片并未完全填滿內(nèi)容區(qū)域。 長字幕被截?cái)唷?用戶體驗(yàn)是無聊和靜態(tài)的,因?yàn)樗袉卧翊笮∠嗤?/p>
您可以使用自定義布局改進(jìn)設(shè)計(jì),其中每個(gè)單元格可以自由地滿足其需求。
Creating Custom Collection View Layouts
您將首先為圖庫創(chuàng)建自定義布局類,從而創(chuàng)建令人驚嘆的集合視圖(collection view)。
集合視圖布局是抽象類UICollectionViewLayout的子類。 它們定義集合視圖中每個(gè)項(xiàng)目的可視屬性。
各個(gè)屬性是UICollectionViewLayoutAttributes的實(shí)例。 它們包含集合視圖中每個(gè)項(xiàng)目的屬性,例如項(xiàng)目的frame或transform。
在Layouts組中創(chuàng)建一個(gè)新文件。 從iOS ? Source列表中選擇Cocoa Touch Class。 將其命名為PinterestLayout并使其成為UICollectionViewLayout的子類。
接下來,配置集合視圖以使用新布局。 打開Main.storyboard。 在Photo Stream View Controller Scene中選擇Collection View,如下所示:

接下來,打開Attributes inspector。 在Layout下拉列表中選擇Custom。 然后在Class下拉列表中選擇PinterestLayout:

好的 - 是時(shí)候看一下它的樣子了。 構(gòu)建并運(yùn)行您的應(yīng)用:


別恐慌! 信不信由你,這是一個(gè)好兆頭。
這意味著集合視圖正在使用您的自定義布局類。 單元格未顯示,因?yàn)?code>PinterestLayout尚未實(shí)現(xiàn)布局過程中涉及的任何方法。
Core Layout Process
想想集合視圖布局過程。 它是集合視圖和布局對象之間的協(xié)作。 當(dāng)集合視圖需要一些布局信息時(shí),它會要求您的布局對象通過按特定順序調(diào)用某些方法來提供它:

您的布局子類必須實(shí)現(xiàn)以下方法:
-
collectionViewContentSize:此方法返回集合視圖內(nèi)容的寬度和高度。您必須實(shí)現(xiàn)它以返回整個(gè)集合視圖內(nèi)容的高度和寬度,而不僅僅是可見內(nèi)容。集合視圖在內(nèi)部使用此信息來配置其滾動視圖的內(nèi)容大小。 -
prepare():每當(dāng)布局操作即將發(fā)生時(shí),UIKit都會調(diào)用此方法。這是您準(zhǔn)備和執(zhí)行確定集合視圖大小和項(xiàng)目位置所需的任何計(jì)算的機(jī)會。 -
layoutAttributesForElements(in :):在此方法中,返回給定矩形內(nèi)所有項(xiàng)的布局屬性。您將屬性作為UICollectionViewLayoutAttributes數(shù)組返回到集合視圖。 -
layoutAttributesForItem(at :):此方法向集合視圖提供按需布局信息。您需要覆蓋它并在請求的indexPath處返回該項(xiàng)的布局屬性。
好的,所以你知道你需要實(shí)現(xiàn)什么。但是你如何計(jì)算這些屬性呢?
Calculating Layout Attributes
對于此布局,您需要?jiǎng)討B(tài)計(jì)算每個(gè)項(xiàng)目的高度,因?yàn)槟孪炔恢勒掌母叨取?您將聲明一個(gè)協(xié)議,當(dāng)PinterestLayout需要它時(shí),它將提供此信息。
現(xiàn)在,回到代碼。 打開PinterestLayout.swift。 在PinterestLayout類之前添加以下委托協(xié)議聲明:
protocol PinterestLayoutDelegate: AnyObject {
func collectionView(
_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
}
此代碼聲明了PinterestLayoutDelegate協(xié)議。 它有一種方法來請求照片的高度。 您很快就會在PhotoStreamViewController中實(shí)現(xiàn)此協(xié)議。
在實(shí)現(xiàn)布局方法之前還有一件事要做。 您需要聲明一些有助于布局過程的屬性。
將以下內(nèi)容添加到PinterestLayout:
// 1
weak var delegate: PinterestLayoutDelegate?
// 2
private let numberOfColumns = 2
private let cellPadding: CGFloat = 6
// 3
private var cache: [UICollectionViewLayoutAttributes] = []
// 4
private var contentHeight: CGFloat = 0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
// 5
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
此代碼定義了稍后您需要提供布局信息的一些屬性。這是一步一步解釋的:
- 1) 這保留了對代理的引用。
- 2) 這些是用于配置布局的兩個(gè)屬性:列數(shù)和單元格填充。
- 3) 這是一個(gè)用于緩存計(jì)算屬性的數(shù)組。當(dāng)您調(diào)用
prepare()時(shí),您將計(jì)算所有項(xiàng)的屬性并將它們添加到緩存中。當(dāng)集合視圖稍后請求布局屬性時(shí),您可以有效地查詢緩存,而不是每次都重新計(jì)算它們。 - 4) 這聲明了兩個(gè)屬性來存儲內(nèi)容大小。在添加照片時(shí)增加
contentHeight,并根據(jù)集合視圖寬度及其內(nèi)容插入計(jì)算contentWidth。 - 5)
collectionViewContentSize返回集合視圖內(nèi)容的大小。您可以使用前面步驟中的contentWidth和contentHeight來計(jì)算大小。
您已準(zhǔn)備好計(jì)算集合視圖項(xiàng)的屬性?,F(xiàn)在,它將由frame組成。要了解您將如何執(zhí)行此操作,請查看下圖:

您將根據(jù)每個(gè)項(xiàng)目的列以及同一列中上一個(gè)項(xiàng)目的位置來計(jì)算每個(gè)項(xiàng)目的frame。 您可以通過跟蹤frame的xOffset和上一個(gè)項(xiàng)目的位置yOffset來完成此操作。
您將首先使用項(xiàng)目所屬列的起始X坐標(biāo)來計(jì)算水平位置,然后添加單元格填充。 垂直位置是該列中前一項(xiàng)的起始位置,加上該前一項(xiàng)的高度。 整體項(xiàng)目高度是圖像高度和內(nèi)容填充的總和。
你將在prepare()中做到這一點(diǎn)。 您的主要目標(biāo)是為布局中的每個(gè)項(xiàng)計(jì)算UICollectionViewLayoutAttributes的實(shí)例。
將以下方法添加到PinterestLayout:
override func prepare() {
// 1
guard
cache.isEmpty,
let collectionView = collectionView
else {
return
}
// 2
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
// 3
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 4
let photoHeight = delegate?.collectionView(
collectionView,
heightForPhotoAtIndexPath: indexPath) ?? 180
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column],
y: yOffset[column],
width: columnWidth,
height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// 5
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
依次記錄每個(gè)編號的的代碼:
- 1) 如果緩存
cache為空且集合視圖存在,則只計(jì)算布局屬性。 - 2) 根據(jù)列寬度為每列聲明并填充
xOffset數(shù)組。yOffset數(shù)組跟蹤每列的y位置。您將yOffset中的每個(gè)值初始化為0,因?yàn)檫@是每列中第一個(gè)項(xiàng)目的偏移量。 - 3) 遍歷第一
section中的所有項(xiàng)目,因?yàn)榇颂囟ú季种挥幸粋€(gè)部分。 - 4) 執(zhí)行
frame計(jì)算。width是先前計(jì)算的cellWidth,其中刪除了單元格之間的填充。向代理請求照片的高度,然后根據(jù)此高度和頂部和底部的預(yù)定義cellPadding計(jì)算frame高度。如果沒有設(shè)置代理,請使用默認(rèn)單元格高度。然后,將其與當(dāng)前列的x和y偏移量組合,以創(chuàng)建屬性使用的insetFrame。 - 5) 創(chuàng)建
UICollectionViewLayoutAttributes的實(shí)例,使用insetFrame設(shè)置其frame并將屬性附加到cache。 - 6) 展開
contentHeight以考慮新計(jì)算項(xiàng)目的frame。然后,根據(jù)frame推進(jìn)當(dāng)前列的yOffset。最后,推進(jìn)column,以便下一個(gè)項(xiàng)目放在下一列中。
注意:由于只要集合視圖的布局變得無效就會調(diào)用
prepare(),因此在典型實(shí)現(xiàn)中有許多情況需要在此處重新計(jì)算屬性。例如,當(dāng)方向更改時(shí),UICollectionView的邊界可能會更改。如果從集合中添加或刪除項(xiàng)目,它們也可能會更改。
現(xiàn)在您需要重寫layoutAttributesForElements(in :)。集合視圖在prepare()之后調(diào)用它以確定哪些項(xiàng)在給定矩形中可見。
將以下代碼添加到PinterestLayout的最后:
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
在這里,您遍歷cache中的屬性并檢查它們的frame是否與集合視圖提供的rect相交。
使用與該rect相交的frame向visibleLayoutAttributes添加任何屬性,最終返回到集合視圖。
您必須實(shí)現(xiàn)的最后一個(gè)方法是layoutAttributesForItem(at :)。
override func layoutAttributesForItem(at indexPath: IndexPath)
-> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
在這里,您從cache中檢索并返回與請求的indexPath對應(yīng)的布局屬性。
Connecting with UIViewController
在您可以看到正在運(yùn)行的布局之前,您需要實(shí)現(xiàn)布局代理。 PinterestLayout依賴于此來計(jì)算項(xiàng)目frame高度時(shí)的照片和標(biāo)題高度。
打開PhotoStreamViewController.swift。 將以下擴(kuò)展名添加到文件末尾以實(shí)現(xiàn)PinterestLayoutDelegate:
extension PhotoStreamViewController: PinterestLayoutDelegate {
func collectionView(
_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
return photos[indexPath.item].image.size.height
}
}
在這里,您可以為布局提供照片的精確高度。
接下來,在viewDidLoad()中添加以下代碼,在super調(diào)用的正下方:
if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
這將PhotoStreamViewController設(shè)置為您的布局的代理。
是時(shí)候再看一下了! 構(gòu)建并運(yùn)行您的應(yīng)用程序。 您將看到根據(jù)照片的高度正確定位和調(diào)整單元格:

通過比您想象的更少的工作,您已經(jīng)創(chuàng)建了自己的Pinterest式自定義布局!
如果您想了解有關(guān)自定義布局的更多信息,請考慮以下資源:
- 閱讀Collection View Programming Guide for iOS的Creating Custom Layouts部分,該部分詳細(xì)介紹了此主題。
后記
本篇主要講述了UICollectionView的自定義布局,感興趣的給個(gè)贊或者關(guān)注~~~
