這篇文章有
- collection view自定義布局的一些心得體會和查閱文檔時的一些筆記
- Compositional layout筆記 (少量)
- diffable datasource筆記
Compositional Layout
- Group 寬高給夠(或estimate),Item固定大小,就成了一個FlowLayout
- 設定section垂直方向行為為滾動(分頁,靠邊等),則不會折行
-
.continuousGroupLeadingBoundary的意思是如果一行擺不下,正常情況下會折行,這一行后面就會剩下空白,當你做成continous后,下一個元素也會排在空白后,而不是直接就接在后面了 -
.paging和.groupPageing的區(qū)別則是一次滾動一頁還是一個group
-
Diffable Data Sources
- A diffable data source stores a list of section and item identifiers
- In contrast, a custom data source that conforms to
UICollectionViewDataSourceuses indices and index paths, which aren’t stable. - They represent the location of sections and items, which can change as the data source adds, removes, and rearranges the contents of a collection view.
- 相反Diffable Data Source卻能根據identifier追溯到其location
- In contrast, a custom data source that conforms to
- To use a value as an identifier, its data type must conform to the
Hashableprotocol.- Hashing能讓集合成為“鍵”,提供快速lookup能力
- 比如set, dictionary, snapshot
- can determine the differences between its current snapshot and another snapshot.
- Hashing能讓集合成為“鍵”,提供快速lookup能力
Define the Diffable Data Source
@preconcurrency @MainActor class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, SectionIdentifierType : Sendable, ItemIdentifierType : Hashable, ItemIdentifierType : Sendable
// 聲明示例
private var recipeListDataSource: UICollectionViewDiffableDataSource<RecipeListSection, Recipe.ID>!
private enum RecipeListSection: Int {
case main
}
struct Recipe: Identifiable, Codable {
var id: Int
var title: String
var prepTime: Int // In seconds.
var cookTime: Int // In seconds.
var servings: String
var ingredients: String
var directions: String
var isFavorite: Bool
var collections: [String]
fileprivate var addedOn: Date? = Date()
fileprivate var imageNames: [String]
}
- section是枚舉,枚舉就是正整數
- Recipe conforming to
Identifiable,automatically exposes the associated typeID - 整個
Recipe結構體不必是Hashable的,因為存在Datasource和Snapshot里的僅僅只是identifiers- Using the
Recipe.IDas the item identifier type for therecipeListDataSourcemeans that the data source, and any snapshots applied to it, contains onlyRecipe.IDvalues and not the complete recipe data.
- Using the
Configure the Diffable Data Source
// Create a cell registration that the diffable data source will use.
let recipeCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Recipe> { cell, indexPath, recipe in
// 會帶著cell對象,位置和應的數據源數據來請求配置當前cell
// 這里進行了兩種配置,
// 1. 一種是對contentConfiguration進行配置(應該就是包了一層,沒對cell暴露出來的subview直接進行設置)
var contentConfiguration = UIListContentConfiguration.subtitleCell()
contentConfiguration.text = recipe.title
contentConfiguration.secondaryText = recipe.subtitle
contentConfiguration.image = recipe.smallImage
contentConfiguration.imageProperties.cornerRadius = 4
contentConfiguration.imageProperties.maximumSize = CGSize(width: 60, height: 60)
cell.contentConfiguration = contentConfiguration
// 2. 這里就是直接對cell的subview來進行設置了,所以理論上上一節(jié)的內容應該也可以直接對cell來配置
if recipe.isFavorite {
let image = UIImage(systemName: "heart.fill")
let accessoryConfiguration = UICellAccessory.CustomViewConfiguration(customView: UIImageView(image: image), placement: .trailing(displayed: .always), cell.accessories = [.customView(configuration: accessoryConfiguration)]
} else {
cell.accessories = []
}
}
// Create the diffable data source and its cell provider.
recipeListDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, identifier -> UICollectionViewCell in
// `identifier` is an instance of `Recipe.ID`. Use it to
// retrieve the recipe from the backing data store.
let recipe = dataStore.recipe(with: identifier)!
// 這里既是傳入注冊cell的方法的地方,也是那個方法的handler里三個參數的來源
return collectionView.dequeueConfiguredReusableCell(using: recipeCellRegistration, for: indexPath, item: recipe)
}
- The
configureDataSource()method creates a cell registration and provides a handler closure that configures each cell with data from a recipe.
Load the Diffable Data Source with Identifiers
private func loadRecipeData() {
// Retrieve the list of recipe identifiers determined based on a
// selected sidebar item such as All Recipes or Favorites.
guard let recipeIds = recipeSplitViewController.selectedRecipes?.recipeIds()
else { return }
// Update the collection view by adding the recipe identifiers to
// a new snapshot, and apply the snapshot to the diffable data source.
var snapshot = NSDiffableDataSourceSnapshot<RecipeListSection, Recipe.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(recipeIds, toSection: .main)
recipeListDataSource.applySnapshotUsingReloadData(snapshot) // 初始化用這個,reload代表完全重設
// 更新的話用 apply(_:animatingDifferences:) 這樣有動畫
}
Insert, Delete, and Move Items
- To handle changes to a data collection, the app creates a new snapshot that represents the current state of the data collection and applies it to the diffable data source.
- The data source compares its current snapshot with the new snapshot to determine the changes.
- Then it performs the necessary inserts, deletes, and moves into the collection view based on those changes.
var snapshot = NSDiffableDataSourceSnapshot<RecipeListSection, Recipe.ID>()
snapshot.appendSections([.main]) // section是直接重建的,而不是從哪去retrieve一個, 因為它代表的是ID,只要值一致就行
snapshot.appendItems(selectedRecipeIds, toSection: .main) // 這里是.main的全量數據,即增刪后的結果集
recipeListDataSource.apply(snapshot, animatingDifferences: true)
- 增刪其實就是新建一個snapshot,datasource會根據identifiers來比較哪些多了哪些少了。
- 因為只比較“數量“,所以只要用這些id去新建snapshot就可以了,不存在把舊的retrieve出來
Update Existing Items
- To handle changes to the properties of an EXISTING item, an app retrieves the current snapshot from the diffable data source and calls either
reconfigureItems(_:)orreloadItems(_:)on the snapshot. -> thenApplyto snapshot
var snapshot = recipeListDataSource.snapshot() // 這次是retrieve了
// Update the recipe's data displayed in the collection view.
snapshot.reconfigureItems([recipeId]) // 傳入identifier
recipeListDataSource.apply(snapshot, animatingDifferences: true)
- the data source invokes its cell provider closure,
Populate Snapshots with Lightweight Data Structures
- 對整個item對象做Hash,適用于快速建模,或數據源不會變更的場景(比如菜單)。
- 因為item對象的任何屬性變化都會被認為有過改動導致重繪,也會產生一些副作用,比如重繪之前的狀態(tài)都會被清掉(如selected)
- 實踐中,不會對設置datasource的時候專門給個identifier集合,而數據源用別的集合,每次都是用identifier從集合里找item這種方式,而是重寫item的hash方法和equal方法,讓其只觀察id字段
NSDiffableDataSourceSnapshot
- A representation of the state of the data in a
viewat a specific point in time. - Diffable data sources use snapshots to provide data for collection views and table views.
- You use a snapshot to set up the initial state of the data that a view displays, and you use snapshots to reflect changes to the data that the view displays.
- The data in a snapshot is made up of the sections and items
- Each of your sections and items must have unique identifiers that conform to the
Hashableprotocol.
- Each of your sections and items must have unique identifiers that conform to the
// Create a snapshot.
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
// Populate the snapshot.
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()])
// Apply the snapshot.
dataSource.apply(snapshot, animatingDifferences: true)
NSDiffableDataSourceSectionSnapshot
-
A representation of the state of the data in a
layout sectionat a specific point in time.- 注意與
dataSourceSnapshot定義的區(qū)別
- 注意與
A section snapshot represents the data for a single section in a collection view or table view.
Through a section snapshot, you set up the initial state of the data that displays in an individual section of your view, and later update that data.
You can use section snapshots with or instead of an
NSDiffableDataSourceSnapshotUse a section snapshot when you need precise management of the data in a section of your layout
such as when the sections of your layout acquire their data from different sources.
不同的section來自不同的數據源的話,傾向于用sectionSnapshot
for section in Section.allCases {
// Create a section snapshot
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<String>()
// Populate the section snapshot
sectionSnapshot.append(["Food", "Drinks"])
sectionSnapshot.append(["??", "??", "??"], to: "Food")
// Apply the section snapshot
dataSource.apply(sectionSnapshot,
to: section,
animatingDifferences: true)
}
蘋果CollectionView教程文檔
The Layout Object Controls the Visual Presentation
- The layout object is solely responsible for determining the placement and visual styling of items within the collection view
- do not confuse what a layout object does with the
layoutSubviewsmethod used to reposition child views inside a parent view. - A layout object never touches the views it manages directly because it does not actually own any of those views.
- it generates attributes that describe the location, size, and visual appearance of the cells, supplementary views, and decoration views in the collection view.
- It is then the job of the collection view to apply those attributes to the actual view objects.
- 這就是需要提供兩個代理方法的原因,一個提供view,一個提供布局配置
Transitioning Between Layouts
- The easiest way to transition between layouts is by using the
setCollectionViewLayout:animated:method. - However, if you require control of the transition or want it to be interactive, use a
UICollectionViewTransitionLayoutobject. - The
UICollectionViewTransitionLayoutclass is a special type of layout that gets installed as the collection view’s layout object when transitioning to a new layout.- With a transition layout object, you can have objects follow a non linear path, use a different timing algorithm, or move according to incoming touch events.
- The
UICollectionViewLayoutclass provides several methods for tracking the transition between layouts. -
UICollectionViewTransitionLayoutobjects track the completion of a transition through thetransitionProgressproperty. - As the transition occurs, your code updates this property periodically to indicate the completion percentage of the transition.
通用流程:
- Create an instance of the standard class or your own custom class using the
initWithCurrentLayout:nextLayout:method. - Communicate the progress of the transition by periodically modifying the
transitionProgressproperty. Do not forget to invalidate the layout using the collection view’sinvalidateLayoutmethod after changing the transition’s progress. - Implement the
collectionView:transitionLayoutForOldLayout:newLayout:method in your collection view’s delegate and return your transition layout object. - Optionally modify values for your layout using the
updateValue:forAnimatedKey:method to indicate changed values relevant to your layout object. The stable value in this case is 0.
Customizing the Flow Layout Attributes
- Flowlayout在一條線上排列元素,到達了邊界就換行,新起一條線
- 元素大小可以通過
itemSize屬性設置,如果大小不同,則通過[collectionView:layout:sizeForItemAtIndexPath:](https://developer.apple.com/documentation/uikit/uicollectionviewdelegateflowlayout/1617708-collectionview)代理方法設置 - 但是,同一行上不同的高度的cell會垂直居中排列,這點要注意
-
minimum spacing設置的只是同一行元素的“最小間距”,如果布局的時候一行下一個元素放不下了,但是剩余的空間很多,這個一行的元素間距會拉大- 行間距同理,根據上一條描述,元素是垂直居中排列的,所以最小行間距設置的是上下兩行間最高的元素的距離