collectionview,diffable datasource一些筆記

這篇文章有

  • 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 UICollectionViewDataSource uses 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
  • To use a value as an identifier, its data type must conform to the Hashable protocol.
    • Hashing能讓集合成為“鍵”,提供快速lookup能力
      • 比如set, dictionary, snapshot
    • can determine the differences between its current snapshot and another snapshot.

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]
}
  1. section是枚舉,枚舉就是正整數
  2. Recipe conforming to Identifiable,automatically exposes the associated type ID
  3. 整個Recipe結構體不必是Hashable的,因為存在Datasource和Snapshot里的僅僅只是identifiers
    1. Using the Recipe.ID as the item identifier type for the recipeListDataSource means that the data source, and any snapshots applied to it, contains only Recipe.ID values and not the complete recipe data.

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(_:) or reloadItems(_:) on the snapshot. -> then Apply to 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 view at 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 Hashable protocol.
// 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 section at 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 NSDiffableDataSourceSnapshot

  • Use 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 layoutSubviews method 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 UICollectionViewTransitionLayout object.
  • The UICollectionViewTransitionLayout class 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 UICollectionViewLayout class provides several methods for tracking the transition between layouts.
  • UICollectionViewTransitionLayout objects track the completion of a transition through the transitionProgress property.
  • As the transition occurs, your code updates this property periodically to indicate the completion percentage of the transition.

通用流程:

  1. Create an instance of the standard class or your own custom class using the initWithCurrentLayout:nextLayout: method.
  2. Communicate the progress of the transition by periodically modifying the transitionProgress property. Do not forget to invalidate the layout using the collection view’s invalidateLayout method after changing the transition’s progress.
  3. Implement the collectionView:transitionLayoutForOldLayout:newLayout: method in your collection view’s delegate and return your transition layout object.
  4. 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設置的只是同一行元素的“最小間距”,如果布局的時候一行下一個元素放不下了,但是剩余的空間很多,這個一行的元素間距會拉大
    • 行間距同理,根據上一條描述,元素是垂直居中排列的,所以最小行間距設置的是上下兩行間最高的元素的距離
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容