天天看點

WWDC 2018 :CollectionView 之旅

本文是 WWDC 2018 Session 225 讀後感,其視訊及配套 PDF文稿 位址如下 A Tour Of UICollectionView。

這篇文章難度不大,由易到難,逐層深入,是一篇很好的 Session。全文總計約2500字,通讀全文花費時間大約15分鐘。

看完這篇 Session,給我的直覺感受是這篇名為 A Tour Of UICollectionView 的文章,是圍繞着一個 CollectionView 的案例,對自定義布局以及其性能優化、資料操作、動畫做的一次探讨。雖然沒有新增的 API 和特性,但是實際意義蠻大。

我們也按照 Session 的思路,将本文主要分為三個子產品:

  • CollectionView 概述
  • 布局(自定義 Layout)
  • 資料的重新整理、動畫

CollectionView 想必各位已經不陌生了,在我們的日常開發中,它的身影随處可見。如果還有小夥伴對它不熟悉,可以看看之前的 Session :

  • WWDC 2016 - What`s New In CollectionView In iOS 10 。
  • WWDC 2017 - Drag and Drop with Collection and Table View。

如果我們想搭建一個如下圖的 App ,需要涉及到三點:布局、重新整理、動畫,我們今天的話題也是圍繞着這三點展開。

CollectionView 概述

CollectionView 的核心概念有三點:布局(Layout)、資料源(Data Source)、代理(Delegate)。

UICollectionViewLayout

UICollectionViewLayout 負責管理 UICollectionViewLayoutAttributes,一個 UICollectionViewLayoutAttributes 對象管理着一個 CollectionView 中一個 Item 的布局相關屬性。包括 Bounds、center、frame 等。同時要注意在當 Bounds 在改變時是否需要重新整理 Layout, 以及布局時的動畫。

UICollectionViewFlowLayout

UICollectionViewFlowLayout 是 UICollectionViewLayout 的子類,是系統提供給我們一個封裝好的流式布局的類。

橫向流式布局(白色線代表布局方向)

縱向流式布局(白色線代表布局方向)

這種流式布局需要區分方向,方向不同,具體的 Line Spacing 和 Item Spacing 所代表的含義不同,具體差異,可以通過上面的兩張圖進行區分。

因為流式布局其強大的适用性,是以在設計中這種布局方式被廣泛使用。

UICollectionViewDataSource

資料源:顧名思義,提供資料的分組資訊、每組中 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
複制代碼
           

UICollectionViewDelegate

delegate 提供了一些細顆粒度的方法:

  • Highlighting
  • Selection

還有一些視圖的顯示事件:

  • willDisplayItem
  • didEndDisplayingItem

布局 - 自定義 Layout

系統提供的

UICollectionViewFlowLayout

雖然使用起來友善快捷,能夠滿足基本的布局需要。但是遇到如下圖的布局樣式,顯然就無法達到我們所需的效果,這時就需要自定義

FlowLayout

了。

自定義

FlowLayout

并不複雜 ,有以下四步:

1.提供滾動範圍
override var collectionViewContentSize: CGSize 
複制代碼
           
2.提供布局屬性對象
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 

func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
複制代碼
           
3.布局的相關準備工作
// 為每個 invalidateLayout 調用
// 緩存 UICollectionViewLayoutAttributes
// 計算 collectionViewContentSize
func prepare()
複制代碼
           
4.處理自定義布局中的邊界更改
// 在 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: ...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[].isUpdated = true
    
    let movedPerson = people[]
    people.remove(at:)
    people.remove(at:)
    
    people.insert(movedPerson, at:)
    
    // Update Collection View
    collectionView.reloadItems(at: [IndexPath(item:, section:)])
    collectionView.reloadItems(at: [IndexPath(item:, section:)])
    collectionView.moveItem(at: IndexPath(item:, section:), to:IndexPath(item:, section:))
}
複制代碼
           

這個例子在操作過程中報錯,原因如下:我們删除和移動的是同一個索引位置的元素。我們顯示地調用了

reloadData()

,

reloadData()

是一個異步執行的函數,會直接通路資料源方法,進行重新布局,多次調用容易出錯,同時這樣寫也沒有動畫效果。

performBatchUpdates

上面出錯的場景其實挺常見,為了規範操作,避免在編輯的場景下出現問題,應當将對

CollectionView

的新增、删除、重新整理、插入等操作都放入到

performBatchUpdates()

中的

updates

閉包内,

CollectionView

中 Item 的更新順序我們不需要關心,但是資料源更新的順序是很重要的。

首先認識一下這個方法

func performBatchUpdates( updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil)

.其中 updates 閉包内部會執行新增、删除、重新整理、插入等一系列操作。

.而 completion 閉包會在 updates 閉包執行完畢後開始執行,updates 閉包中的相關操作會觸發一些動畫,
  當這些動畫執行成功會傳回 True,當動畫被打斷或者執行失敗會傳回 false,這個參數也有可能會傳回 nil。
複制代碼
           

這個方法可以用來對

collectionView

中的元素進行批量的新增、删除、重新整理、插入等操作,同時将觸發

collectionView

layout

的對應動畫:

.func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?

.func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: NSCollectionView.DecorationElementKind, at decorationIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?

.func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?

.func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
複制代碼
           

原因是因為在執行完

performBatchUpdates

操作之後,CollectionView 會自動

reloadData

調用資料源方法重新布局。是以我們在 Updates 閉包中對資料的編輯操作執行完畢後,一定要同步更新資料源,否則有極大的幾率出現資料越界等錯誤情況。

易出錯的合并更新一般有以下幾種

  • 1.移動與删除同一個索引
  • 2.移動與插入同一個索引
  • 3.将多個對象移動到同一個索引
  • 4.引用了一個無效的索引

既然在執行操作時容易出現問題,我們就該想辦法去規避,蘋果的工程師給出了很好的建議。在上面我們講過對

CollectionView

的新增、删除、重新整理、插入等操作都放入到

performBatchUpdates()

中的

updates

閉包内,

CollectionView

中 Item 的更新順序我們不需要關心,但是資料源更新的順序很重要。最後的 Item 更新順序和資料源的更新順序是怎麼回事呢?

你可以這樣了解:

  • 在 Updates 閉包内,你可以選擇先删除一個索引,然後插入一個新的索引,或是把兩者的順序颠倒過來進行操作,這都沒有問題,你可以按照自己的喜好,随意指定順序。
  • 但是涉及到資料源更新的方法,必須按照一定的順序和規則來操作。

資料源執行操作的順序及規則

  • 1.将移動操作拆分成删除和插入。
  • 2.将所有的删除操作合并到一起,同理将所有的插入操作也合并到一起。
  • 3.以降序優先處理删除操作。
  • 4.最後以升序處理插入操作。

然後我們将剛才出錯的代碼,改為如下:

// 新的實作
func performUpdates() {
    
    UIView.performWithoutAnimation {
        // 先将資料重新整理
        CollectionView.performBatchUpdates({
            people[].isUpdate = true
            CollectionView.reloadItems(at: [IndexPath(item:, section:)])
        })
        
        // 再将移動拆分成删除之後再插入兩個動作
        CollectionView.performBatchUpdates({
            let movedPerson = people[]
            people.remove(at: )
            people.remove(at: )
            people.insert(movedPerson, at:)
            CollectionView.deleteItems(at: [IndexPath(item:, section:)])
            collectionView.moveItem(at: IndexPath(item:, section:), to:IndexPath(item:, section:))
        })
    }
}
複制代碼
           

最後總結一下,蘋果的工程師建議我們通過自定義布局來實作精美的布局樣式,同時采取二分查找的方式來高效的處理資料,提升界面的流暢性和使用者體驗。

其次對 CollectionView 的操作建議我們通過

performBatchUpdates

來進行處理,我們不需要去考慮動畫的執行,因為預設都幫助我們處理好了,我們隻需要注意資料源處理的原則和順序,確定資料處理的安全與穩定。

如果對這篇 Session 很感興趣的話,可以在 Twitter 上聯系作者,隻需要在 Twitter 搜尋 A Tour Of CollectionView 即可,作者還是很熱心的。

最後聲明,筆者的英語聽力比較慘,有些地方聽得不是特别明白,一旦發現我的資訊有遺漏或者傳達的資訊有誤,還望大家不吝指教。

檢視更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

轉載于:https://juejin.im/post/5b1b8cd3e51d4506d536799e