前言
UITableView 和 UICollectionView 是我們開發(fā)者最常用的控件了,大量的流式布局需要這兩個控件來實現(xiàn)虑瀑,因此這兩個控件也是 Apple 重點優(yōu)化的對象。在往屆 WWDC 中滴须,我們已經(jīng)受益于 UITableViewDataSourcePrefetching 舌狗、優(yōu)化版 Autolayout 等帶來的性能提升,以及 UITableViewDragDelegate 帶來的原生拖拽功能扔水。今年痛侍,Apple 帶來了全新的 Compositional Layout 。它將徹底顛覆 UICollectionView 的布局體驗魔市,大大拓展 UICollectionView 的可塑性主届。
背景
早期的 App 設(shè)計相對簡單,使用 UICollectionViewFlowLayout 可以應(yīng)付大多數(shù)使用場景待德。而隨著應(yīng)用的發(fā)展君丁,越來越多的頁面趨于復(fù)雜化,UICollectionViewFlowLayout 在面對復(fù)雜布局往往會顯得力不從心将宪,或者非常復(fù)雜绘闷,需要進行大量的計算和判斷橡庞。而自由度更高的 UICollectionViewLayout 則有著更高的接入門檻,稍有不慎還容易出現(xiàn)各種各樣的 bug 印蔗。
我們就拿 App Store為例扒最,它包含了大小不一的
Item
,以及可以上下华嘹、左右滑動的交互吧趣。假如你是開發(fā)者,你會如何搭建這個 UI 除呵?你可能會使用多個 UICollectionView 嵌套在一個 UIScrollerView 中再菊,因為 UICollectionView 的滾動軸只能有一個(橫向 / 豎向)。但如果我告訴你颜曾,在新版 iOS 13 中纠拔,這個頁面只使用了一個 UICollectionView ,你會有什么感覺泛豪。你一定很好奇它是怎么做到的稠诲。其中的秘密就是 Compositional Layout 。
介紹
Compositional Layout 是此次隨 iOS 13 一同發(fā)布的全新 UICollectionView 布局诡曙。它的目標(biāo)有三個:
- Composable 可組合的
- Flexible 靈活的
- Fast 快
為了達到上面這三個目標(biāo)臀叙,Compositional Layout 在原有 UICollectionViewLayout Item
Section
的基礎(chǔ)上,增加了一層 Group
的概念价卤。多個 Item
組成一個 Group
劝萤,多個 Group
組成一個 Section
。
說了這么多慎璧,還不如上代碼
// Create a List by Specifying Three Core Components: Item, Group and Section
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44.0))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
可以看到床嫌,為了能夠?qū)?fù)雜的布局描述清楚,我們需要創(chuàng)建多個類來分別描述 Item
胸私、 Group
厌处、 Section
的大小、間距等屬性岁疼。
如何解讀上面這段代碼阔涉?
- 首先
Item
的高度為44定高,寬度是父視圖(Group
)寬度的 100% 捷绒。 -
Group
的尺寸描述使用了和Item
完全相同的的 size 瑰排,即高度為44定高,寬度是父視圖(Section
)寬度的 100% 疙驾。 -
Section
的寬度是 UICollectionView的寬度凶伙,高度默認(rèn)為其Group
所有元素渲染出來的總高度,即Group
的高度它碎。 - 最終函荣,我們會通過 Frame 或 AutoLayout對 UICollectionView 進行尺寸設(shè)置显押。
通過上面的解析,你能夠在腦中勾畫出這個 UICollectionView 長什么樣子嗎傻挂?好吧乘碑,其實我也不能,但好在我能夠跑一下代碼看下實際但結(jié)果金拒。
結(jié)果就是一個類似 UITableView 的布局兽肤。
好吧,我承認(rèn)這有點難绪抛。因為我們看代碼的順序都是從上而下资铡,但假如 Compositional Layout 層級的尺寸依賴于父視圖,我們就不得不結(jié)合父視圖和自身的布局來推倒出最終的布局幢码,這需要一定的空間想象力笤休。
在上面這個例子中,每一個 “UITableViewCell” 就是一個 Item
症副,也是一個 Group
店雅,而整個 “UITableViewCell” 只包含了一個 Section
。
所以看到這里你一定會好奇贞铣,我們?yōu)槭裁葱枰?Group
這么一個東西闹啦?很抱歉我需要將這個疑問留到最后。
核心布局
我們先來談?wù)勛罨A(chǔ)的核心布局辕坝。
在詳細(xì)介紹 Compositional Layout 中用到的四大類之前窍奋,我們需要先來了解一下,一個新的用于描述尺寸大小的類酱畅。
NSCollectionLayoutDimension
過去费变,我們可以使用 CGSize 來描述一個固定大小的 Item
。后來圣贸,我們擁有了 estimatedItemSize
來描述一個動態(tài)計算大小的 Item
,并且給它一個預(yù)估的值扛稽。但更多的時候吁峻,為了適配不同的屏幕尺寸,我們需要根據(jù)屏幕的寬度手動計算出 Item
的大性谡拧(比如限定一行只顯示3個 Item
)用含。
如何用簡潔優(yōu)雅的方式去描述上面三種場景呢?答案是 NSCollectionLayoutDimension
class NSCollectionLayoutDimension {
class func fractionalWidth(_ fractionalWidth: CGFloat) -> Self
class func fractionalHeight(_ fractionalHeight: CGFloat) -> Self
class func absolute(_ absoluteDimension: CGFloat) -> Self
class func estimated(_ estimatedDimension: CGFloat) -> Self
}
NSCollectionLayoutDimension
添加了根據(jù)父視圖的比例來描述尺寸的 fractionalWidth / fractionalHeight 的方法帮匾,并將定值啄骇、自適應(yīng)、比例這三大描述方式統(tǒng)一分裝了起來瘟斜。
我們來看一個例子缸夹。
let size = NSCollectionLayoutDimension(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalWidth(0.25))
}
如圖痪寻,使用簡單的描述,我們就可以得到以父視圖(
Item
的父視圖為 Group
)為基準(zhǔn)的比例尺寸虽惭。它不僅被用于描述 Item
的大小橡类,同樣也用于 Group
。
了解完這個基礎(chǔ)之后芽唇,讓我們看看 NSCollectionLayoutDimension 是如何在 Compositional Layout 中發(fā)揮作用的顾画。
-
NSCollectionLayoutSize
class NSCollectionLayoutSize { init(widthDimension: NSCollectionLayoutDimension, }
單純用于描述
Item
的大小,使用到了上面介紹的 NSCollectionLayoutDimension匆笤。 -
NSCollectionLayoutItem
class NSCollectionLayoutItem { convenience init(layoutSize: NSCollectionLayoutSize) var contentInsets: NSDirectionalEdgeInsets }
用于描述一個
Item
的完整布局信息研侣,包含了上面的尺寸 NSCollectionLayoutSize ,以及邊距 NSDirectionalEdgeInsets炮捧。 -
NSCollectionLayoutGroup
class NSCollectionLayoutGroup: NSCollectionLayoutItem { class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self class func vertical(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self class func custom(layoutSize: NSCollectionLayoutSize, itemProvider: NSCollectionLayoutGroupCustomItemProvider) -> Self }
用于描述
Group
布局庶诡。它也提供了垂直 / 水平兩種方向。同時你也可以實現(xiàn) NSCollectionLayoutGroupCustomItemProvider 自定義Group
的布局方式寓盗。它同樣接收一個 NSCollectionLayoutDimension 灌砖,用于確定
Group
的大小。需要注意的是傀蚌,當(dāng)Item
使用了 fractionalWidth / fractionalHeight 時基显,Group
的大小會影響Item
的大小。此外善炫,它還有一個 subitems 參數(shù)撩幽,類型為 NSCollectionLayoutItem 數(shù)組,用于傳遞
Item
箩艺。 -
NSCollectionLayoutSection
class NSCollectionLayoutSection { convenience init(layoutGroup: NSCollectionLayoutGroup) var contentInsets: NSDirectionalEdgeInsets }
用于描述
Section
布局信息窜醉。同樣可以通過修改 contentInsets 來改變Section
的邊距。
以上就是用于描述 Compositional Layout 用到的四個類艺谆。通過對布局的精確描述榨惰,我們就能夠得到可塑性非常強的 UICollectionView布局,而無需重寫復(fù)雜的 UICollectionViewLayout 静汤。不過琅催,Compositional Layout 的可玩性還不止于此,如果想要進一步的自定義虫给,需要使用到一些額外的高級布局技巧藤抡。
高級布局
NSCollectionLayoutAnchor
對于 Item 而言,我們可能會有類似 iOS 桌面小圓點的需求抹估。通過 NSCollectionLayoutAnchor 缠黍,我們可以很容易的給 Item 添加自定義小控件。
// NSCollectionLayoutAnchor
let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing],
fractionalOffset: CGPoint(x: 0.3, y: -0.3))
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),
heightDimension: .absolute(20))
let badge = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind: "badge", containerAnchor: badgeAnchor)
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])
同樣是通過多個類來分別描述 Anchor 的方位药蜻、大小和視圖瓷式,我們就可以非常方便地為 Item 添加自定義錨替饿。
NSCollectionLayoutBoundarySupplementaryItem
Headers 和 Footers 是也我們經(jīng)常用到的組件,這次 Compositional Layout 弱化了 Header 和 Footer 的概念蒿往,他們都是 NSCollectionLayoutBoundarySupplementaryItem
盛垦,只不過你可以通過描述其相對于 Section
的位置(top / bottom)來達到過去 Header 和 Footer 的效果。
// NSCollectionLayoutBoundarySupplementaryItem
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "header", alignment: .top)
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: "footer", alignment: .bottom)
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header, footer]
pinToVisibleBounds
屬性則是用來描述 NSCollectionLayoutBoundarySupplementaryItem
劃出屏幕后是否留在 CollectionView 的最上端瓤漏,也就是之前 Plain style
的 Header 樣式腾夯。
NSCollectionLayoutDecorationItem
有沒有遇到過這樣的UI需求?
以往要實現(xiàn)這樣的樣式往往會非常復(fù)雜蔬充,而如今我們終于可以自定義 Section 的背景啦蝶俱。
// Section Background Decoration Views
let background = NSCollectionLayoutDecorationItem.background(elementKind: "background")
section.decorationItems = [background]
// Register Our Decoration View with the Layout
layout.register(MyCoolDecorationView.self, forDecorationViewOfKind: "background")
通過NSCollectionLayoutDecorationItem
,我們可以為 Section
的背景添加自定義視圖饥漫,其加載方式和 Item
Header
Footer
一樣通過榨呆,需要先 register
。
Estimated Self-Sizing
在添加了如此多自定義特性之后庸队,Compositional Layout 依舊支持自適應(yīng)尺寸积蜻。這極大方便了我們對動態(tài)內(nèi)容的展示,同時對 Dynamic text 這類系統(tǒng)特性也能有更好的支持彻消。
// Estimated Self-Sizing
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44.0))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
header.pinToVisibleBounds = true
elementKind: "header",
alignment: .top)
section.boundarySupplementaryItems = [header, footer]
Nested NSCollectionLayoutGroup
不知道你有沒有發(fā)現(xiàn)竿拆,NSCollectionLayoutGroup
初始化方法中的 subitems
參數(shù)類型為 NSCollectionLayoutItem
數(shù)組,而 NSCollectionLayoutGroup
同樣繼承自 NSCollectionLayoutItem
宾尚,也就是說丙笋,NSCollectionLayoutGroup
內(nèi)可以嵌套 NSCollectionLayoutGroup
。這樣作的目的是煌贴,通過嵌套 Group
我們可以自定義出層級更加復(fù)雜的布局御板。
這個 Group 用代碼如何描述?
// Nested NSCollectionLayoutGroup
let leadingItem = NSCollectionLayoutItem(layoutSize: leadingItemSize) let trailingItem = NSCollectionLayoutItem(layoutSize: trailingItemSize)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize) subitem: trailingItem, count: 2)
let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize, subitems: [leadingItem, trailingGroup])
想一想如此復(fù)雜的布局如果自己去實現(xiàn) UICollectionViewLayout 將會是多么復(fù)雜牛郑,如今通過簡潔而抽象的 Compositional Layout API 我們可以非常直觀的描述這一布局怠肋。
Orthogonal Scrolling Sections
這個特性就是我們前面提到的,讓 Section 可以滾動起來的特性淹朋。
// Orthogonal Scrolling Sections
section.orthogonalScrollingBehavior = .continuous
通過設(shè)置 Section 的 orthogonalScrollingBehavior 參數(shù)灶似,我們可以實現(xiàn)多種不同的滾動方式。
// Orthogonal Scrolling Sections
enum UICollectionLayoutSectionOrthogonalScrollingBehavior: Int {
case none
case continuous
case continuousGroupLeadingBoundary
case paging
case groupPaging
case groupPagingCentered
}
orthogonalScrollingBehavior
參數(shù)是一個 UICollectionLayoutSectionOrthogonalScrollingBehavior
類型的枚舉瑞你,包含了我們在實際開發(fā)者會用到的幾乎所有滾動方式,比如常見的自由滾動希痴,按page滾動者甲,以及按 Group 滾動(包含以 Group Leading 為邊界和以 Group Center 為邊界)。以往要實現(xiàn)類似的效果砌创,我們大多需要自己實現(xiàn) UICollectionViewLayout 或者干脆求助類似 AnimatedCollectionViewLayout 這樣的第三方庫虏缸,如今 Apple 已經(jīng)為你全部實現(xiàn)鲫懒!
而如果我希望做一個類似 App Store 中部這樣滾動的布局呢?
這會稍稍有些復(fù)雜刽辙。首先窥岩,如果你仔細(xì)閱讀文檔,你會發(fā)現(xiàn) NSCollectionLayoutGroup 有一個我們之前沒有提到的 API 宰缤。
open class func vertical(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self
它相比默認(rèn)的 API 颂翼,subitem
不再接收數(shù)組而只接收單一的 Item
(意味著這個模式下,Group
不支持多種大小的 Item
或 Item
+ Group
的組合慨灭,但聰明的你一定想到了可以先構(gòu)建一個組合的 Group
然后傳進這個 API 中)朦乏,同時多了一個 count
。這個 count
會讓 Group
嘗試在其限定的大小內(nèi)塞入 count
個數(shù)的 Item
氧骤。最終達到的效果就是類似
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item, item, item])
不過上面的代碼不會生效呻疹,因為 subitems
關(guān)注的是不同的 Item
的組合,而非實際 Item
的個數(shù)筹陵,因此 subitems
會對數(shù)組內(nèi)的 Item 去重刽锤。因此如果你希望在一個 Group 中塞入多個 Item,后者是你唯一的選擇朦佩。
看到這里你是否對 Group 的作用有了一點感覺并思?上面的例子中,如果我們關(guān)閉 Section 的滾動功能吕粗,那么會是什么樣子的纺荧?
每個
Group
中還是會有 3 個 Item
,只不過由于 Section
的寬度限制颅筋,下一個 Group
不得不排布到上一個 Group
的下放宙暇,結(jié)果展示出來的還是一個類似 TableView 的布局。當(dāng)我們打開 Section
的滾動模式议泵,奇跡發(fā)生了占贫。由于 Section
可以滾動,因此它存在類似于 ScrollerView 的 ContentView
先口,它的子 View
可以在更大的范圍內(nèi)渲染型奥,因此之后的 Group
可以跟隨在之前的 Group
右側(cè),并最終填充 Section 的整個 ContentView碉京。
現(xiàn)在你該知道 Apple 為什么要引入 Group
的概念了吧厢汹。其實我在看 Advances in Collection View Layout 的時候也是悶的,直到最后看到了 App Store 的例子我才明白了谐宙,為了能夠?qū)崿F(xiàn)多緯度的滾動(實際上是賦予了 Section
滾動的特性)烫葬,原有的層級就不足以描述一個完整的多維度 CollectionView ,需要一個額外的層級來描述位于 Section
和 Item
的中間層。這樣說可能會略顯生澀搭综,大家可以把現(xiàn)在的 Section
想象成原來的 CollectionView 垢箕,而新的 Group
就是原來的 Section
。由于現(xiàn)在 Section
充當(dāng)了之前 CollectionView 的角色被賦予了滾動的特性兑巾,因此需要一個額外的層級來描述之前 Section
所描述的 “一組 Item
的” 關(guān)系 条获。 Group
便由此出現(xiàn)。
可以說 Group 的存在是完全服務(wù)于這個可滾動 Section 的蒋歌∷Ь颍可滾動的 Section 為 CollectionView 增加了一個緯度的信息流,如果你的 CollectionView 沒有多維滾動的需求奋姿,那么你會發(fā)現(xiàn)使用 Compositional Layout 的 Group 是一個完全沒有必要的事情锄开。
復(fù)習(xí)
正如我前面所說,Compositional Layout 的層級關(guān)系依次是 Item > Group > Section > Layout 称诗。
理解了這其中的層級關(guān)系和特性萍悴,能夠幫助你寫出更靈活、性能更好的 UI 寓免!
總結(jié)
Compositional Layout 為我們帶來了更加可塑易用的 CollectionView 布局以及多維度瀑布流癣诱,對于 UICollectionView 而言是一個全新的升級,它將賦予 UICollectionView 更多的可能性袜香。不過限于 iOS 13 的版本限制撕予,我們還需要一段時間才能真正用上它,不過我已經(jīng)等不及了蜈首。
官方的Demo实抡,幾乎展示了Compositional Layout 的所有布局,支持 iOS 和 macOS欢策。強烈推薦大家跟著代碼和結(jié)果走一遍吆寨!
Using Collection View Compositional Layouts and Diffable Data Sources