iOS 中的 CompositionalLayout 呐馆、 DiffableDataSource(更新至iOS14)

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)嫉晶,如圖:

布局結(jié)構(gòu)

item:可以理解為UICollectionViewCell骑疆,布局的最小單元。
group: 布局組合層车遂,用于組合 item 的布局封断,其自身也能夠嵌套(把被嵌套的group當成一個item進行布局),為布局提供更多可能舶担。有垂直坡疼、水平、自定義三種方式衣陶,繪制時group并不會對視圖層級造成影響柄瑰。
section: 布局中每一段的布局定義闸氮,是group的容器,還提供了header教沾、footer蒲跨、附加視圖等功能∈诜可通過orthogonalScrollingBehavior 指定 section 的滾動方式

舉一個簡單的 Banner 布局的例子熟悉下上述各部分內(nèi)容:


Banner效果圖

從圖中可以看出或悲,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)。


增加Item

實現(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)在就熟悉這些特性正好合適

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者呀洲。
  • 序言:七十年代末紊选,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子道逗,更是在濱河造成了極大的恐慌兵罢,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滓窍,死亡現(xiàn)場離奇詭異趣些,居然都是意外死亡,警方通過查閱死者的電腦和手機贰您,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門坏平,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锦亦,你說我怎么就攤上這事舶替。” “怎么了杠园?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵顾瞪,是天一觀的道長。 經(jīng)常有香客問我,道長陈醒,這世上最難降的妖魔是什么惕橙? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮钉跷,結(jié)果婚禮上弥鹦,老公的妹妹穿的比我還像新娘。我一直安慰自己爷辙,他們只是感情好彬坏,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著膝晾,像睡著了一般栓始。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上血当,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天幻赚,我揣著相機與錄音,去河邊找鬼臊旭。 笑死落恼,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的巍扛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼乏德,長吁一口氣:“原來是場噩夢啊……” “哼撤奸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喊括,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤胧瓜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后郑什,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體府喳,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年蘑拯,在試婚紗的時候發(fā)現(xiàn)自己被綠了钝满。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡申窘,死狀恐怖弯蚜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剃法,我是刑警寧澤碎捺,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響收厨,放射性物質(zhì)發(fā)生泄漏晋柱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一诵叁、第九天 我趴在偏房一處隱蔽的房頂上張望雁竞。 院中可真熱鬧,春花似錦黎休、人聲如沸浓领。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽联贩。三九已至,卻和暖如春捎拯,著一層夾襖步出監(jiān)牢的瞬間泪幌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工署照, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留祸泪,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓建芙,卻偏偏與公主長得像没隘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子禁荸,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353