(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

jamesdouble發表於2018-07-03

起步基礎

  • UICollectionViewLayout 基本使用

  • UICollectionViewLayoutAttributes

Attributes賦值

這裡泛指了以下兩個主函式,就不在贅述兩個功能,以及 UICollectionViewLayoutAttributes 需處理的變數。

class AutoSizingLayout: UICollectionViewLayout {

	override func prepare() {
		super.prepare()
	}
	
	override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
		return attributes
	}

}
複製程式碼

Without Invalidating

先來看看在 沒有需要重新改變Attributes 下的流程(以下簡稱 配置流程):

Without Invalidating

data reload時,prepare計算一次,layoutAttributesForElements呼叫多次直到,系統已經有所有IndexPath的atrribute,就不會在呼叫這些functiom,直到collectionView reload。

With Invalidating

現在我們把失效的概念加進來

強制失效 UICollectionViewLayout.invalidateLayout()

invalidateLayout()可隨時呼叫,他會將所有系統已取得的 Attribute 全部標記為 invalid 並捨棄。

準確的update時機並不是呼叫後,而是在下一次 layout 的 update Cycle裡後,重新呼叫prepare. 堆疊如圖:

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617728-invalidatelayout

若有overrider此方法,必須call super.invalidateLayout()

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

條件失效 UICollectionViewLayout.shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617781-shouldinvalidatelayout

預設回傳 false。

overrider後,可藉由傳入的 newBounds 判斷是否需要 invalidLayout,若回傳 true 則跟 InvalidateLayout()之後的流程(堆疊)相同。

例如內容下半部,需要不斷更新Attribute

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
	guard let collectionView = collectionView else { return false }
	if newBounds.maxY > contentSize.height / 2 {
   		return true
	}
	return false
}
複製程式碼

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

newBounds: CGRect

此bounds的觸發時機,為collectionView可視範圍改動時(contentOffset Change)

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

為了簡化流程圖,我們將固定一起出現的這幾個步驟,劃成一個:

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

UICollectionViewLayoutInvalidationContext

(以下簡稱 InvalidationContext) https://developer.apple.com/documentation/uikit/uicollectionviewlayoutinvalidationcontext

這的context跟出現在其他地方的Context上下文概念差不多,先借由一個function的引數,對此上下文進行設定,回傳後再下一個function對設定的內容進行處理。

基於系統『原生』的 InvalidationContext 失效layout

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

這裡再多覆寫了兩個函式

  • invalidationContext(forBoundsChange:)

    可以藉由引數 bounds 對context 進行部分邏輯處理,也可在這做 『失效標記』

    https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617781-shouldinvalidatelayout

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
		let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
		return invalidationContext
}

複製程式碼
  • invalidateLayout(with:)

    根據上一部處理好的邏輯或 『失效標記』 做屬性處理,必呼叫super.invalidateLayout(with: invalidationContext)

    (iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
    	//以context的資訊,做些update
    }
    複製程式碼

系統提供可用的失效標記,無任何標記將會重新進入『配置流程』

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

這些標記的功用,是在第一個function根據bounds作上標記,在第二個funciton中可以根據以下對應的變數取得當初的標記,做對應的『區域性屬性預處理』。

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

有標記可做 區域性屬性預處理 ,並會被重新詢問 Attribute

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

重新詢問 Attribute

例如:若滑超過 1/2 Y,使 row 17 失效。


override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
	let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
	if newBounds.maxY > contentSize.height {
		invalidationContext.invalidateItems(at: [IndexPath(row: 17, section: 0)])
    }
    return invalidationContext
}
複製程式碼

覆寫的此function就會被呼叫並詢問 row17 的attribute

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return row17's attribute
}
複製程式碼

區域性屬性預處理

區域性屬性預處理 其實就是為了上一步 『重新詢問 Attribute』這塊做預先計算,

例如:若滑超過 1/2 Y,使 某Decoration失效。

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
	let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
	if newBounds.maxY > contentSize.height {
		invalidationContext.invalidateDecorationElements(
                ofKind: "Footer",
                at: [IndexPath(item: 1, section: 0)]
            )
    }
    return invalidationContext
}

複製程式碼

並在 invalidateLayout(with:) 從context裡查詢是否對應的Decoration包含在失效名單內,並提前計算好心的Attribute存在持有變數

override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
	let invalidationContext = context
	if let dic = context.invalidatedDecorationIndexPaths, let idx = dic["Footer"] {
        prepareFooterViewAttributes()
    }
}
複製程式碼

在詢問的時候,將預先計算好的Attribute 回傳


override public func layoutAttributesForDecorationView(
        ofKind elementKind: String,
        at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return self.footerLayoutAttributes
}

複製程式碼

特殊標記 (get-only)

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

這兩個特殊標記是會在觸發 collectionView.reloadData()時會被系統自動啟用,不能自己設定,並且仍會重新進入『配置流程』。

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

(iOS) UICollectionViewLayoutInvalidationContext效能優化 詳細流程圖 + 範例

基於系統『自定義』的 InvalidationContext 失效layout

第一步當然是寫一個繼承UICollectionViewLayoutInvalidationContext的類,並且在UICollectionViewLayout類裡覆寫以下

override class var invalidationContextClass: AnyClass {
    return InvalidationContext子類名.self
}
複製程式碼

自定義 InvalidationContext 的好處不外乎就是能自己增加欄位,能更清晰也更有邏輯的銜接前後兩個函式

例如:沿用前面的例子,但InvalidationContext為自定義,增加一個Bool,判斷哪部分需要失效或是需要被標記

class LayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
    var invalidateFooter = false
}

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
    let invalidationContext = super.invalidationContext(forBoundsChange: newBounds) as! LayoutInvalidationContext
    guard let collectionView = collectionView else { return invalidationContext }
    let originChanged = !collectionView.bounds.origin.equalTo(newBounds.origin)
    if originChanged && newBounds.maxY > contentSize.height {
        invalidationContext.invalidateFooter = true
    }
    return invalidationContext
}

override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
    let invalidationContext = context as! LayoutInvalidationContext
    if invalidationContext.invalidateFooter {
        prepareFooterViewAttributes()
        invalidationContext.invalidateDecorationElements(
            ofKind: "Footer",
            at: [IndexPath(item: 1, section: 0)]
        )
    }
    super.invalidateLayout(with: invalidationContext)
}
複製程式碼

相關文章