起步基礎
UICollectionViewLayout 基本使用
UICollectionViewLayoutAttributes
Attributes賦值
這裡泛指了以下兩個主函式,就不在贅述兩個功能,以及 UICollectionViewLayoutAttributes 需處理的變數。
class AutoSizingLayout: UICollectionViewLayout {
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributes
}
}
複製程式碼
Without Invalidating
先來看看在 沒有需要重新改變Attributes 下的流程(以下簡稱 配置流程):

data reload時,prepare計算一次,layoutAttributesForElements
呼叫多次直到,系統已經有所有IndexPath的atrribute,就不會在呼叫這些functiom,直到collectionView reload。
With Invalidating
現在我們把失效的概念加進來
強制失效 UICollectionViewLayout.invalidateLayout()
invalidateLayout()可隨時呼叫,他會將所有系統已取得的 Attribute 全部標記為 invalid 並捨棄。
準確的update時機並不是呼叫後,而是在下一次 layout 的 update Cycle裡後,重新呼叫prepare. 堆疊如圖:

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617728-invalidatelayout
若有overrider此方法,必須call super.invalidateLayout()

條件失效 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
}
複製程式碼

newBounds: CGRect
此bounds的觸發時機,為collectionView可視範圍改動時(contentOffset Change)
為了簡化流程圖,我們將固定一起出現的這幾個步驟,劃成一個:

UICollectionViewLayoutInvalidationContext
(以下簡稱 InvalidationContext) https://developer.apple.com/documentation/uikit/uicollectionviewlayoutinvalidationcontext
這的context跟出現在其他地方的Context上下文概念差不多,先借由一個function的引數,對此上下文進行設定,回傳後再下一個function對設定的內容進行處理。
基於系統『原生』的 InvalidationContext 失效layout

這裡再多覆寫了兩個函式
-
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)
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { //以context的資訊,做些update } 複製程式碼
系統提供可用的失效標記,無任何標記將會重新進入『配置流程』

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

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

重新詢問 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)

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


基於系統『自定義』的 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)
}
複製程式碼