本文是 WWDC 2018 Session 225 讀後感,其視頻及配套 PDF文稿 地址以下 A Tour Of UICollectionView。swift
這篇文章難度不大,由易到難,逐層深刻,是一篇很好的 Session。全文總計約2500字,通讀全文花費時間大約15分鐘。數組
看完這篇 Session,給個人直觀感覺是這篇名爲 A Tour Of UICollectionView 的文章,是圍繞着一個 CollectionView 的案例,對自定義佈局以及其性能優化、數據操做、動畫作的一次探討。雖然沒有新增的 API 和特性,可是實際意義蠻大。緩存
咱們也按照 Session 的思路,將本文主要分爲三個模塊:安全
CollectionView 想必各位已經不陌生了,在咱們的平常開發中,它的身影隨處可見。若是還有小夥伴對它不熟悉,能夠看看以前的 Session :性能優化
若是咱們想搭建一個以下圖的 App ,須要涉及到三點:佈局、刷新、動畫,咱們今天的話題也是圍繞着這三點展開。閉包
CollectionView 的核心概念有三點:佈局(Layout)、數據源(Data Source)、代理(Delegate)。app
UICollectionViewLayout 負責管理 UICollectionViewLayoutAttributes,一個 UICollectionViewLayoutAttributes 對象管理着一個 CollectionView 中一個 Item 的佈局相關屬性。包括 Bounds、center、frame 等。同時要注意在當 Bounds 在改變時是否須要刷新 Layout, 以及佈局時的動畫。異步
UICollectionViewFlowLayout 是 UICollectionViewLayout 的子類,是系統提供給咱們一個封裝好的流式佈局的類。ide
這種流式佈局須要區分方向,方向不一樣,具體的 Line Spacing 和 Item Spacing 所表明的含義不一樣,具體差別,能夠經過上面的兩張圖進行區分。函數
由於流式佈局其強大的適用性,因此在設計中這種佈局方式被普遍使用。
數據源:顧名思義,提供數據的分組信息、每組中 Item 數量以及每一個 Item
的實際內容。
optional func numberOfSections(in collectionView: UICollectionView) -> Int
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
複製代碼
delegate 提供了一些細顆粒度的方法:
還有一些視圖的顯示事件:
系統提供的 UICollectionViewFlowLayout
雖然使用起來方便快捷,可以知足基本的佈局須要。可是遇到以下圖的佈局樣式,顯然就沒法達到咱們所需的效果,這時就須要自定義 FlowLayout
了。
自定義 FlowLayout
並不複雜 ,有如下四步:
override var collectionViewContentSize: CGSize
複製代碼
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
複製代碼
// 爲每一個 invalidateLayout 調用
// 緩存 UICollectionViewLayoutAttributes
// 計算 collectionViewContentSize
func prepare()
複製代碼
// 在 CollectionView 滾動時是否容許刷新佈局
func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
複製代碼
經過以上的方法,咱們能夠輕鬆實現自定義 layout
的佈局。可是在實際開發中,有一個對性能提高很實用的小技巧很值得咱們借鑑。
一般,咱們獲取當前屏幕上全部顯示的 UICollectionViewLayoutAttributes
會這麼寫
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedAttributes.filter { (attributes:UICollectionViewLayoutAttributes) -> Bool in
return rect.intersects(attributes.frame)
}
}
複製代碼
採用以上的寫法,咱們會遍歷緩存了全部 UICollectionViewLayoutAttributes 的 cachedAttributes 數組。而隨着用戶的拖動屏幕,這個方法會被頻繁的調用,也就是會作大量的計算。當 cachedAttributes 數組的量級達到必定的規模,對性能的負面影響就會很是明顯,用戶在使用過程當中會出現卡頓的負面體驗。
蘋果工程師採用的辦法能夠很好地解決這一問題。全部的 UICollectionViewLayoutAttributes 都按照順序被存儲在 cachedAttributes 數組中,既然是一個有序的數組,那麼只要咱們經過二分查找,拿到任何一個在當前頁面顯示的 Attribures 對象,就能夠以這個 Attribures 對象爲中心,向前向後遍歷查找符合條件的 Attribures 對象便可,這樣查找的範圍就被大大縮小了。相應地,計算量變小,對性能的提高很是明顯。
爲了讓你們易於理解,畫了一張圖,雖然有點醜,但表達思想足夠了。 當前顯示的 CollectionView 的範圍就是 rect。在 rect 內部經過二分查找,找到第一個合適的 UICollectionViewLayoutAttributes 做爲 firstMatchIndex,也就是那個 Attributes 對象。
在 rect 內, firstMatchIndex 以上的 Attributes 都符合 attributes.frame.maxY >= rect.minY
,而在 firstMatchIndex 如下的 Attributes 也都符合 attributes.frame.maxY <= rect.maxY
的條件。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
// 找到在當前區域內的任何一個 Attributes 的 Index
guard let firstMatchIndex = binarySearchAttributes(range: 0...cachedAttributes.endIndex, rect:rect) else { return attributesArray }
// 從後向前反向遍歷,縮小查找範圍
for attributes in cachedAttributes[..<firstMatchIndex].reversed {
guard attributes.frame.maxY >= rect.minY else {break}
attributesArray.append(attributes)
}
// 從前向後正向遍歷,縮小查找範圍
for attributes in cachedAttributes[firstMatchIndex...] {
guard attributes.frame.minY <= rect.maxY else {break}
attributesArray.append(attributes)
}
return attributesArray
}
複製代碼
經過二分查找的方式,在處理當前頁面顯示的 UICollectionViewLayoutAttributes
的過程當中能夠減小遍歷的數據量,在實際體驗中頁面滑動更加順滑,體驗更好,這種處理 Attribures
對象的方式,值得咱們在開發過程當中借鑑。
咱們會遇到對 CollectionView
進行編輯的場景,編輯操做通常是新增、刪除、刷新、插入等。在本 Session 中,主講人爲咱們作了一個示例。
爲了便於理解,仍是貼一下代碼吧:
// 原函數
func performUpdates() {
people[3].isUpdated = true
let movedPerson = people[3]
people.remove(at:3)
people.remove(at:2)
people.insert(movedPerson, at:0)
// Update Collection View
collectionView.reloadItems(at: [IndexPath(item:3, section:0)])
collectionView.reloadItems(at: [IndexPath(item:2, section:0)])
collectionView.moveItem(at: IndexPath(item:3, section:0), to:IndexPath(item:0, section:0))
}
複製代碼
這個例子在操做過程當中報錯,緣由以下:咱們刪除和移動的是同一個索引位置的元素。咱們顯示地調用了 reloadData()
, reloadData()
是一個異步執行的函數,會直接訪問數據源方法,進行從新佈局,屢次調用容易出錯,同時這樣寫也沒有動畫效果。
上面出錯的場景其實挺常見,爲了規範操做,避免在編輯的場景下出現問題,應當將對 CollectionView
的新增、刪除、刷新、插入等操做都放入到 performBatchUpdates()
中的 updates
閉包內,CollectionView
中 Item 的更新順序咱們不須要關心,可是數據源更新的順序是很重要的。
首先認識一下這個方法
func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil)
1.其中 updates 閉包內部會執行新增、刪除、刷新、插入等一系列操做。
2.而 completion 閉包會在 updates 閉包執行完畢後開始執行,updates 閉包中的相關操做會觸發一些動畫,
當這些動畫執行成功會返回 True,當動畫被打斷或者執行失敗會返回 false,這個參數也有可能會返回 nil。
複製代碼
這個方法能夠用來對 collectionView
中的元素進行批量的新增、刪除、刷新、插入等操做,同時將觸發collectionView
的 layout
的對應動畫:
1.func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
2.func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: NSCollectionView.DecorationElementKind, at decorationIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
3.func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
4.func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
複製代碼
緣由是由於在執行完 performBatchUpdates
操做以後,CollectionView 會自動 reloadData
調用數據源方法從新佈局。因此咱們在 Updates 閉包中對數據的編輯操做執行完畢後,必定要同步更新數據源,不然有極大的概率出現數據越界等錯誤狀況。
既然在執行操做時容易出現問題,咱們就該想辦法去規避,蘋果的工程師給出了很好的建議。在上面咱們講過對 CollectionView
的新增、刪除、刷新、插入等操做都放入到 performBatchUpdates()
中的 updates
閉包內,CollectionView
中 Item 的更新順序咱們不須要關心,可是數據源更新的順序很重要。最後的 Item 更新順序和數據源的更新順序是怎麼回事呢?
你能夠這樣理解:
而後咱們將剛纔出錯的代碼,改成以下:
// 新的實現
func performUpdates() {
UIView.performWithoutAnimation {
// 先將數據刷新
CollectionView.performBatchUpdates({
people[3].isUpdate = true
CollectionView.reloadItems(at: [IndexPath(item:3, section:0)])
})
// 再將移動拆分紅刪除以後再插入兩個動做
CollectionView.performBatchUpdates({
let movedPerson = people[3]
people.remove(at: 3)
people.remove(at: 2)
people.insert(movedPerson, at:0)
CollectionView.deleteItems(at: [IndexPath(item:2, section:0)])
collectionView.moveItem(at: IndexPath(item:3, section:0), to:IndexPath(item:0, section:0))
})
}
}
複製代碼
最後總結一下,蘋果的工程師建議咱們經過自定義佈局來實現精美的佈局樣式,同時採起二分查找的方式來高效的處理數據,提高界面的流暢性和用戶體驗。
其次對 CollectionView 的操做建議咱們經過 performBatchUpdates
來進行處理,咱們不須要去考慮動畫的執行,由於默認都幫助咱們處理好了,咱們只須要注意數據源處理的原則和順序,確保數據處理的安全與穩定。
若是對這篇 Session 很感興趣的話,能夠在 Twitter 上聯繫做者,只須要在 Twitter 搜索 A Tour Of CollectionView 便可,做者仍是很熱心的。
最後聲明,筆者的英語聽力比較慘,有些地方聽得不是特別明白,一旦發現個人信息有遺漏或者傳達的信息有誤,還望你們不吝指教。
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄