iOS13 Compositional Layout幫你實現(xiàn)collecion的各種布局

前言

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ù)雜,需要進(jìn)行大量的計算和判斷。而自由度更高的 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 快

為了達(dá)到上面這三個目標(biāo),Compositional Layout 在原有 UICollectionViewLayout?Item?Section?的基礎(chǔ)上,增加了一層?Group?的概念。多個?Item?組成一個?Group?,多個?Group?組成一個?Section?。

說了這么多,還不如上代碼

//?Create?a?List?by?Specifying?Three?Core?Components:?Item,?Group?and?Sectionlet?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)復(fù)制代碼

可以看到,為了能夠?qū)?fù)雜的布局描述清楚,我們需要創(chuàng)建多個類來分別描述?Item?、?Group?、?Section?的大小、間距等屬性。

如何解讀上面這段代碼?

首先?Item?的高度為44定高,寬度是父視圖(Group)寬度的 100% 。

Group?的尺寸描述使用了和?Item?完全相同的的 size ,即高度為44定高,寬度是父視圖(Section)寬度的 100% 。

Section?的寬度是 UICollectionView的寬度,高度默認(rèn)為其?Group?所有元素渲染出來的總高度。

最終,我們會通過 Frame 或 AutoLayout 對 UICollectionView 進(jìn)行尺寸設(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)?->?Selfclass?func?estimated(_?estimatedDimension:?CGFloat)?->?Self}復(fù)制代碼

NSCollectionLayoutDimension?添加了根據(jù)父視圖的比例來描述尺寸的 fractionalWidth / fractionalHeight 的方法,并將定值、自適應(yīng)、比例這三大描述方式統(tǒng)一分裝了起來。

我們來看一個例子。

let?size?=?NSCollectionLayoutDimension(widthDimension:?.fractionalWidth(0.25),?

???????????????????????????????????????heightDimension:?.fractionalWidth(0.25))

}復(fù)制代碼


如圖,使用簡單的描述,我們就可以得到以父視圖(Item?的父視圖為?Group)為基準(zhǔn)的比例尺寸。它不僅可以被用于描述?Item?的大小,同樣也可以用于?Group。

了解完這個基礎(chǔ)之后,讓我們看看 NSCollectionLayoutDimension 是如何在 Compositional Layout 中發(fā)揮作用的。

NSCollectionLayoutSize

class?NSCollectionLayoutSize?{init(widthDimension:?NSCollectionLayoutDimension,

}復(fù)制代碼

單純用于描述?Item?的大小,使用到了上面介紹的 NSCollectionLayoutDimension。

NSCollectionLayoutItem

class?NSCollectionLayoutItem?{convenience?init(layoutSize:?NSCollectionLayoutSize)var?contentInsets:?NSDirectionalEdgeInsets}復(fù)制代碼

用于描述一個?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}復(fù)制代碼

用于描述?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}復(fù)制代碼

用于描述?Section?布局信息。同樣可以通過修改 contentInsets 來改變?Section?的邊距。

以上就是用于描述 Compositional Layout 用到的四個類。通過對布局的精確描述,我們就能夠得到可塑性非常強的 UICollectionView 布局,而無需重寫復(fù)雜的 UICollectionViewLayout 。不過,Compositional Layout 的可玩性還不止于此,如果想要進(jìn)一步的自定義,需要使用到一些額外的高級布局技巧。

高級布局

NSCollectionLayoutAnchor

對于 Item 而言,我們可能會有類似 iOS 桌面小圓點的需求。通過 NSCollectionLayoutAnchor ,我們可以很容易的給 Item 添加自定義小控件。

//?NSCollectionLayoutAnchorlet?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])復(fù)制代碼

同樣是通過多個類來分別描述 Anchor 的方位、大小和視圖,我們就可以非常方便地為 Item 添加自定義錨。

NSCollectionLayoutBoundarySupplementaryItem

Headers 和 Footers 是也我們經(jīng)常用到的組件,這次 Compositional Layout 弱化了 Header 和 Footer 的概念,他們都是?NSCollectionLayoutBoundarySupplementaryItem?,只不過你可以通過描述其相對于?Section的位置(top / bottom)來達(dá)到過去 Header 和 Footer 的效果。

//?NSCollectionLayoutBoundarySupplementaryItemlet?header?=?NSCollectionLayoutBoundarySupplementaryItem(layoutSize:?headerSize,?elementKind:?"header",?alignment:?.top)let?footer?=?NSCollectionLayoutBoundarySupplementaryItem(layoutSize:?footerSize,?elementKind:?"footer",?alignment:?.bottom)

header.pinToVisibleBounds?=?truesection.boundarySupplementaryItems?=?[header,?footer]復(fù)制代碼

pinToVisibleBounds?屬性則是用來描述?NSCollectionLayoutBoundarySupplementaryItem?劃出屏幕后是否留在 CollectionView 的最上端,也就是之前?Plain style?的 Header 樣式。


NSCollectionLayoutDecorationItem


有沒有遇到過這樣的 UI 需求?

以往要實現(xiàn)這樣的樣式往往會非常復(fù)雜,而如今我們終于可以自定義 Section 的背景啦。

//?Section?Background?Decoration?Viewslet?background?=?NSCollectionLayoutDecorationItem.background(elementKind:?"background")

section.decorationItems?=?[background]//?Register?Our?Decoration?View?with?the?Layoutlayout.register(MyCoolDecorationView.self,?forDecorationViewOfKind:?"background")復(fù)制代碼

通過NSCollectionLayoutDecorationItem?,我們可以為?Section?的背景添加自定義視圖,其加載方式和?ItemHeader?Footer?一樣,需要先?register?。

Estimated Self-Sizing

在添加了如此多自定義特性之后,Compositional Layout 依舊支持自適應(yīng)尺寸。這極大方便了我們對動態(tài)內(nèi)容的展示,同時對 Dynamic text 這類系統(tǒng)特性也能有更好的支持。

//?Estimated?Self-Sizinglet?headerSize?=?NSCollectionLayoutSize(widthDimension:?.fractionalWidth(1.0),

heightDimension:?.estimated(44.0))let?header?=?NSCollectionLayoutBoundarySupplementaryItem(layoutSize:?headerSize,

header.pinToVisibleBounds?=?trueelementKind:?"header",

alignment:?.top)

section.boundarySupplementaryItems?=?[header,?footer]復(fù)制代碼

Nested NSCollectionLayoutGroup

不知道你有沒有發(fā)現(xiàn),NSCollectionLayoutGroup?初始化方法中的?subitems?參數(shù)類型為?NSCollectionLayoutItem?數(shù)組,而?NSCollectionLayoutGroup?同樣繼承自?NSCollectionLayoutItem?,也就是說,NSCollectionLayoutGroup?內(nèi)可以嵌套?NSCollectionLayoutGroup?。這樣作的目的是,通過嵌套?Group我們可以自定義出層級更加復(fù)雜的布局。


這個 Group 用代碼如何描述?

//?Nested?NSCollectionLayoutGrouplet?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ù)制代碼

想一想如此復(fù)雜的布局如果自己去實現(xiàn) UICollectionViewLayout 將會是多么復(fù)雜,如今通過簡潔而抽象的 Compositional Layout API 我們可以非常直觀的描述這一布局。

Orthogonal Scrolling Sections

這個特性就是我們前面提到的,讓 Section 可以滾動起來的特性。

//?Orthogonal?Scrolling?Sectionssection.orthogonalScrollingBehavior?=?.continuous復(fù)制代碼

通過設(shè)置 Section 的 orthogonalScrollingBehavior 參數(shù),我們可以實現(xiàn)多種不同的滾動方式。

//?Orthogonal?Scrolling?Sectionsenum?UICollectionLayoutSectionOrthogonalScrollingBehavior:?Int?{case?nonecase?continuouscase?continuousGroupLeadingBoundarycase?pagingcase?groupPagingcase?groupPagingCentered

}復(fù)制代碼

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復(fù)制代碼

它相比默認(rèn)的 API ,subitem?不再接收數(shù)組而只接收單一的?Item?(意味著這個模式下,Group?不支持多種大小的?Item?或?Item?+?Group?的組合,但聰明的你一定想到了可以先構(gòu)建一個組合的?Group?然后傳進(jìn)這個 API 中),同時多了一個?count。這個?count?會讓?Group?嘗試在其限定的大小內(nèi)塞入?count?個數(shù)的?Item?。最終達(dá)到的效果就是類似

let?group?=?NSCollectionLayoutGroup.vertical(layoutSize:?groupSize,?subitems:?[item,?item,?item])復(fù)制代碼

不過上面的代碼不會生效,因為?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 的??蓾L動的?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上的 App Store 已經(jīng)用上了新的 Compositional Layout ?,不過在 iPad 上旋轉(zhuǎn)動畫的性能不是很好,可見目前版本的 Compositional Layout 還有待優(yōu)化的控件。不過限于 iOS 13 的版本限制,我們還需要一段時間才能真正用上它,但我已經(jīng)等不及了。

官方的Demo,幾乎展示了Compositional Layout 的所有布局,支持 iOS 和 macOS。強烈推薦大家跟著代碼和結(jié)果走一遍!

官方Demo點擊這里

本篇文章引用http://www.cocoachina.com/articles/28730?filter=rec ? 特別感謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容