CollectionView 相關(guān)內(nèi)容:
1. iOS 自定義圖片選擇器 3 - 相冊列表的實現(xiàn)
2. UICollectionView自定義布局基礎(chǔ)
3. UICollectionView自定義拖動重排
4. 本文
5. iOS14 中的UICollectionViewListCell堵泽、UIContentConfiguration 以及 UIConfigurationState
前言:
iOS13 之前, CollectionView 實現(xiàn)主要依靠 Delegate诫钓, DataSource未桥,Layout,三者通力協(xié)作以實現(xiàn)各種各樣的布局類型互妓。
隨著越來越多的應(yīng)用界面越來越復(fù)雜,實現(xiàn)起來耗時耗力坤塞,相似的界面因細微差別卻需要重新寫大量業(yè)務(wù)功能類似的代碼冯勉。而這些界面都有一個共同點:
【界面元素“模塊化”】
類似 AppStore、各種資訊 APP 的主頁一樣摹芙,界面被區(qū)分為多個區(qū)域灼狰,每個區(qū)域有自己單獨的布局特點,蘋果 iOS13 中新增并改良了不少的特性浮禾,以適應(yīng)新的業(yè)務(wù)場景交胚。本文主要以CollectionView為例介紹這些新的特性與使用方式。UITableView中也有對應(yīng)的UITableViewDiffableDataSource盈电,使用方法一樣蝴簇。
UICollectionViewCompositionalLayout
與 UICollectionViewFlowLayout 一樣,UICollectionViewCompositionaLayout 也是基于 UICollectionViewLayout 的布局匆帚,比 FlowLayout 的實現(xiàn)復(fù)雜熬词,也更加靈活。在界面模塊化的場景下更加靈巧吸重。邏輯更清晰互拾。
CompositionalLayout 中,布局主要被劃分為了item嚎幸, group颜矿,section ,這三部分組合成 CompositionalLayout 基本結(jié)構(gòu)嫉晶,如圖:
item:可以理解為UICollectionViewCell骑疆,布局的最小單元。
group: 布局組合層车遂,用于組合 item 的布局封断,其自身也能夠嵌套(把被嵌套的group當成一個item進行布局),為布局提供更多可能舶担。有垂直坡疼、水平、自定義三種方式衣陶,繪制時group并不會對視圖層級造成影響柄瑰。
section: 布局中每一段的布局定義闸氮,是group的容器,還提供了header教沾、footer蒲跨、附加視圖等功能∈诜可通過orthogonalScrollingBehavior 指定 section 的滾動方式
舉一個簡單的 Banner 布局的例子熟悉下上述各部分內(nèi)容:
從圖中可以看出或悲,Banner 在 CollectionView 的第一欄中,能夠左右滑動堪唐,這在之前實現(xiàn)起來稍顯復(fù)雜巡语,嵌套 CollectionView 或是實現(xiàn)自定義 Scrollview 進行大量狀態(tài)控制。而現(xiàn)在淮菠,其布局代碼非常簡單:
//因以模擬器舉例男公,布局為絕對數(shù)值,實際開發(fā)中要注意不同屏幕的適配
var layout: UICollectionViewCompositionalLayout! = nil
var sectionProvider = { (index: Int, enviroment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
// item(藍色矩形合陵,絕大大小 300x200)
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(300), heightDimension: .absolute(200))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// group(組合所有item枢赔,并設(shè)置gorup的內(nèi)邊距)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(320), heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
// section(設(shè)置滾動方向)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
return section
}
//······
layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
上述代碼中的item,group設(shè)置大小均用到了.absolute(XXX)拥知。屬于NSCollectionLayoutDimension 的類方法踏拜,該類提供了多種描述視圖相對布局的方法:
【.fractionalWidth、.fractinalHeight】:
相對于容器寬/高的比例低剔,例如:1表示與容器相等执隧,0.5則表示是容器的一半。
【.absolute】:絕對數(shù)值
【.estimated】:估算大小
Tips:這里要注意 .fractionalWidth 與 .fractinalHeight 相對于容器的概念户侥,在 CompositionalLayout 布局中镀琉,item的相對容器,應(yīng)當是其加入的group蕊唐,group相對容器屋摔,應(yīng)當是其加入的 section 或者另一個 group,
group 可以管理 item 的布局替梨,如間隔钓试,內(nèi)間距等等,因為 Banner 是橫向滾動副瀑,所以使用了group的水平初始化方法 NSCollectionLayoutGroup.horizontal弓熏,其創(chuàng)建了一個水平布局的 group. 對應(yīng)的是垂直布局。
section 根據(jù) group 初始化糠睡,并指定了當前 section 的翻頁方式挽鞠,而在實際開發(fā)中,section 還能做到更多,例如添加附加視圖等信认。
一個Banner材义,總是差點意思,我們可以再實現(xiàn)一個稍微復(fù)雜一點的布局:
這樣的布局嫁赏,可以按照兩個Cell來做其掂,這里我們嘗試用 CompositionalLayout 來實現(xiàn)。
// 右側(cè)小item
let smallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.4))
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
smallItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)
// 右側(cè)group容器
let smallGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1))
let smallGroup = NSCollectionLayoutGroup.vertical(layoutSize: smallGroupSize, subitem: smallItem, count: 2)
smallGroup.interItemSpacing = NSCollectionLayoutSpacing.fixed(10)
// 左側(cè)大item
let bigItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1))
let bigItem = NSCollectionLayoutItem(layoutSize: bigItemSize)
// 容器group(包含了右側(cè)group)
let bigGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(180))
let bigGroup = NSCollectionLayoutGroup.horizontal(layoutSize: bigGroupSize, subitems: [bigItem, smallGroup])
let section = NSCollectionLayoutSection(group: bigGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40)
section.interGroupSpacing = 20
// 設(shè)置背景卡片
let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(
elementKind: CardBackViewKind)
sectionBackgroundDecoration.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)
section.decorationItems = [sectionBackgroundDecoration]
return section
上面使用相對布局來設(shè)定各部分組件的大小潦蝇,并利用group的可嵌套性完成局部的自定義布局款熬。
此處需要注意的是背景視圖需要注冊,與表頭等附加視圖在collectionView上注冊不同攘乒,裝飾視圖是在layout上注冊:
layout.register(CardBackView.self, forDecorationViewOfKind: CardBackViewKind)
UICollectionViewDiffableDataSource
iOS13之前华烟,用 UICollectionViewDataSource 來設(shè)置 CollectionView 有幾行,每行有多少元素持灰,Cell、header等等屬性负饲。其勝在簡易靈活堤魁,但當我們頻繁更新數(shù)據(jù)時,reloadData 太過暴力返十,尤其在需要動畫過渡時妥泉,用戶體驗較差。
iOS13 中新增了 UICollectionViewDiffableDataSource 來幫助我們實現(xiàn)相應(yīng)的功能洞坑。
可以看到盲链,在 DiffableDataSource 中有跟 UICollectionViewDataSource 一樣的方法:
@objc open func numberOfSections(in collectionView: UICollectionView) -> Int
@objc open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
@objc open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
@objc open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
可以將 DiffableDataSource 像以前 UICollectionViewDataSource 一樣類似的方式使用。但若如此的話 DiffableDataSource 也沒有必要當做一門新特性推出了迟杂。在 DiffableDataSource 有一個提交方法:
open func apply(_ snapshot: NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil)
apply 方法提交了一個 NSDiffableDataSourceSnapshot 的結(jié)構(gòu)體...該結(jié)構(gòu)體描述了當前數(shù)據(jù)源的狀態(tài)刽沾,有多少行,多少列排拷。調(diào)用 apply 方法提交新的數(shù)據(jù)源簡要(snapshot)或變更侧漓,程序就會根據(jù) snapshot 更新 collectionView 的狀態(tài)。
實現(xiàn)這樣的效果代碼如下:
var updateSnap = dataSource.snapshot(for: "News")
updateSnap.append([dataSource.snapshot().numberOfItems + 1])
// 此處為 NSDiffableDataSourceSectionSnapshot监氢,iOS14新增特性布蔗,可以對指定的單個 section 的數(shù)據(jù)源進行管理。
dataSource.apply(updateSnap, to: "News", completion: nil)
上面使用append將新數(shù)據(jù)追加在末尾浪腐,也可以使用insert或delete更改數(shù)據(jù)源纵揍,提交后,系統(tǒng)會自動在對應(yīng)位置插入或刪除议街,并附帶過渡動畫泽谨。
數(shù)據(jù)源簡要更新方式具有“簡易、自動化、差異化更新”的特點隔盛,原本需要開發(fā)者計算的狀態(tài)變化交由系統(tǒng)完成犹菱,開發(fā)者只需要提供最新的數(shù)據(jù)源即可。
對于普通場景使用 NSDiffableDataSourceSnapshot 時吮炕,可以通過其提供的快捷屬性來提供 Cell 或附加視圖的代理(CellProvider 與 SupplementaryViewProvider)腊脱。直接在
DiffableDataSource 初始化時就設(shè)置Cell的代理也很簡便。
dataSource = UICollectionViewDiffableDataSource<String, Int>(collectionView: collectionView) { (collectionView, indexPath, _) -> UICollectionViewCell? in
switch indexPath.section {
case 0:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BannerCellID, for: indexPath)
cell.backgroundColor = .blue
return cell
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewsCellID, for: indexPath)
cell.backgroundColor = .orange
return cell
default:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GirlsCellID, for: indexPath)
cell.backgroundColor = .systemPink
return cell
}
}
CompositionalLayout 還有補充視圖(SupplementaryItem)Header 和 Fotter(BoundarySupplementaryItem)龙亲、以及本文用來當做卡片背景的裝飾視圖(DecorationItem)陕凹,更多的內(nèi)容可以下載官方的 Demo 來查看具體的代碼實現(xiàn)。
關(guān)于 CompositionalLayout鳄炉,DiffableDataSource 的簡易介紹就到這里了杜耙,前者是蘋果提供的官方布局,幫開發(fā)者省去了不少的工作量拂盯,后者是一種新的數(shù)據(jù)管理方式佑女。
多說一句:這兩個新增特性特點再結(jié)合最近蘋果對 SwiftUI 的極力推崇,可以看出蘋果對打通Mac iPad iPhone的決心谈竿,以及很早就開始的準備团驱。而完全打通所有平臺最快是明年,到時候應(yīng)該還會新增一些特性空凸,不過大體上的架構(gòu)應(yīng)該不會再變了嚎花,現(xiàn)在就熟悉這些特性正好合適